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());
}
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.