Configuring Efficient Cron Jobs in Drupalfor Drupal 11 , 10 , 9 , and 8

Last updated :  

Cron is easy to treat as invisible — it runs in the background, nobody's watching. But an inefficient hook_cron() implementation runs on every single cron invocation, competing for the exact same database connections and CPU as real visitors. A cron run that takes 30 seconds isn't "free" just because no one's staring at a progress bar; it's 30 seconds of real server load, often at the worst possible moment if it's triggered by page-load-based Automated Cron during peak traffic.

What you'll learn in this lesson

  • The real cost of Drupal's default Automated Cron trigger
  • Why a real system cron job is almost always the better choice
  • How to keep individual hook_cron() implementations fast

The default: Automated Cron

Out of the box, Drupal's core Automated Cron module runs cron opportunistically at the end of a page request, once enough time has passed since the last run. This is convenient for a new site with no server access, but it means an unlucky visitor's page load is the one that pays cron's full execution cost — their request runs noticeably slower for no reason related to what they actually asked for.

The better approach: a real system cron job

# crontab -e
# Run Drupal cron every 15 minutes via drush, outside any visitor's request.
*/15 * * * * cd /var/www/example.com && /usr/local/bin/drush cron >> /var/log/drupal-cron.log 2>&1

With a real system cron job in place, disable the core Automated Cron module (/admin/config/system/cron) so it stops opportunistically triggering — otherwise you're paying the cost twice, on a schedule you don't fully control plus your own explicit one.

Log your cron runs, don't just trust them to work. Redirecting output to a log file, as shown above, is what lets you actually notice when cron starts failing silently — a common real-world failure mode is a cron job that used to work quietly stopping (a changed file path after a server migration, an expired credential) with nobody noticing for weeks because nothing visibly broke.

Keeping individual hook_cron() implementations fast

function mymodule_cron() {
  // Bad: does real, possibly slow work directly inside hook_cron().
  // _mymodule_process_thousands_of_records();

  // Good: hook_cron() just enqueues work; the Queue API (next lesson)
  // processes it incrementally across multiple cron runs.
  $queue = \Drupal::queue('mymodule_processing_queue');
  $queue->createItem(['batch' => 'daily_sync']);
}

Every enabled module's hook_cron() runs on the same cron invocation, one after another. A single module doing genuinely slow synchronous work inside its own hook_cron() delays every other module's cron logic queued to run after it in that same request.

Quick check: your site relies on core's Automated Cron and you notice occasional page loads that feel randomly slow, with no obvious pattern. What's a likely cause worth checking? If you said one of those slow loads happened to be the request that triggered cron, you've connected exactly the failure mode this lesson opened with.

Key takeaways

  • Automated Cron makes an unlucky visitor's request absorb cron's full execution cost — disable it on any real production site.
  • A real system cron job calling drush cron on a fixed schedule runs entirely outside visitor request cycles.
  • Log cron output somewhere you'll actually notice if it silently starts failing.
  • Keep hook_cron() itself fast — enqueue slow work onto the Queue API rather than doing it synchronously inside the hook.

Coming up next

You just saw hook_cron() enqueueing work instead of doing it directly. Next, the Queue API itself — how background tasks actually get processed, and why that separation matters for performance.