Scheduling Lightweight Cron Tasks in Drupalfor Drupal 11 , 10 , 9 , and 8

Last updated :  

Not every scheduled task needs the Queue API's full claim-process-delete machinery. A task that just needs to run "roughly every hour, do one small thing, done" — clearing an expired-token table, pinging a health-check URL — is genuinely simpler as a small, direct hook_cron() check than as a queue with exactly one kind of item. Knowing which category a task falls into keeps your cron implementation as simple as the job actually requires.

What you'll learn in this lesson

  • When a task belongs in hook_cron() directly, versus the Queue API
  • How to throttle a task to run less often than every cron invocation
  • Why State API, not Config, is the right place for "when did this last run"

The decision: hook_cron() directly, or a queue?

  • Direct in hook_cron(): one bounded unit of work, no benefit from being split into individually-retryable pieces (a single DELETE query against an expired-tokens table, say).
  • Queue API: variable, potentially large amounts of work items that benefit from being processed and retried individually (sending N emails, syncing N external records).

A task that's always exactly "one thing" doesn't gain anything from queue machinery — it just adds indirection for no benefit.

Throttling with the State API

function mymodule_cron() {
  $last_run = \Drupal::state()->get('mymodule.last_cleanup', 0);
  $interval = 3600; // Run at most once per hour, however often cron itself fires.

  if (\Drupal::time()->getRequestTime() - $last_run < $interval) {
    return;
  }

  // Do the lightweight work.
  \Drupal::database()->delete('mymodule_expired_tokens')
    ->condition('expires', \Drupal::time()->getRequestTime(), '<')
    ->execute();

  \Drupal::state()->set('mymodule.last_cleanup', \Drupal::time()->getRequestTime());
}
State API, not Config, for "when did this last run": this is the same distinction from the Configuration Management topic in the Module Development tutorial — a last-run timestamp is an environment-specific runtime fact, not something that should be exported and deployed to another environment. Config is for settings a site builder tunes; State is for facts the system tracks about itself.

Why this matters even with a tight cron schedule

If your system cron runs drush cron every 5 minutes (a reasonable interval for queue-processing responsiveness), a task that only needs to run hourly would otherwise run 12x more often than necessary without an explicit throttle — 12x the unnecessary database connections and query overhead for a task that gains nothing from running that frequently.

Quick check: your system cron runs every 5 minutes, but a specific cleanup task only needs to run once a day. Without an explicit throttle inside hook_cron(), how many times more often does it run than necessary? If you said 288 times (24 hours × 12 five-minute intervals per hour), you've got the actual scale of unnecessary work an unthrottled lightweight task can generate.

Key takeaways

  • A task that's always exactly "one bounded unit of work" belongs directly in hook_cron(), not the Queue API.
  • Throttle infrequent tasks explicitly with the State API — don't let them run on every cron invocation just because cron itself runs often.
  • Use State, not Config, for "when did this last run" — it's a runtime fact, not a deployable setting.
  • A frequent cron schedule (good for queue responsiveness) makes explicit throttling more important, not less — otherwise lightweight tasks run far more often than they need to.

Coming up next

You've now covered the complete cron-and-queue performance toolkit — efficient scheduling, background processing, throughput tuning, and lightweight task throttling. Next, we turn to a category of performance work that's less about configuration and more about discipline: auditing which of your own modules and hooks are actually contributing overhead.