skip to content

Since cron fires at most once a minute, how does Laravel's scheduler run a task defined with everyTenSeconds(), and what are the pitfalls?

level: seniorimportance: should knowfreq 30%

answer

  1. schedule:run stays alive all minute
  2. repeat interval must divide 60
  3. 100 ms polling loop
  4. one slow run delays the rest
  5. dispatch a job or run in background

basics

~20 s

When any due task uses a second-based helper, schedule:run does not exit after its first pass: it loops until the end of its minute, re-running each repeating task once its interval has passed. Everything runs sequentially, so slow sub-minute work should be queued.

solid answer

~40 s

`everySecond()`, `everyTwoSeconds()`, `everyFiveSeconds()`, `everyTenSeconds()`, `everyFifteenSeconds()`, `everyTwentySeconds()` and `everyThirtySeconds()` set a repeat interval (it must divide 60 evenly) and make the task due every minute. The cron entry stays the same. When `schedule:run` sees a repeatable task among the due ones, it runs the normal pass and then stays in a loop until the end of the minute it started in, waking every 100 ms and re-running each repeating task whose interval has elapsed — re-checking its filters, the pause flag and maintenance mode each time. The pitfalls: tasks run **sequentially in one process**, so a slow closure delays every other sub-minute task (the docs recommend `Schedule::job()` or a background command); the long-lived process keeps running old code after a deploy until its minute ends; and each minute still costs a full `schedule:run` boot.

code

php · 12 lines
php
<?php

use App\Jobs\HealthPing;
use Illuminate\Support\Facades\Schedule;

// Good: the tick only dispatches; a worker does the HTTP call
Schedule::job(new HealthPing, 'heartbeats')->everyTenSeconds();

// Risky: an inline closure that can take longer than its interval
Schedule::call(function () {
    // slow API call here blocks every other sub-minute task
})->everyFiveSeconds();

go deeper

for a junior

Know that second-based helpers such as everyTenSeconds() exist and need no extra cron entry.

for a middle

Explain the loop: schedule:run stays alive until the end of its minute, polling every 100 ms and re-running repeatable tasks whose interval has passed.

for a senior

Keep sub-minute tasks as thin dispatchers, interrupt in-flight runs on deploy, and watch for timing drift when anything in the loop runs slowly.

for a principal

Decide when periodic triggers should become a queue consumer or a dedicated worker instead of sub-minute schedules, based on load and latency needs.

## The problem sub-minute scheduling solves Cron's finest granularity is one minute, and Laravel's scheduler is driven by a single `* * * * * php artisan schedule:run` entry. A health ping every ten seconds or a queue-depth sample every five seconds cannot be expressed as a cron field. Laravel solves this inside `schedule:run` rather than in cron. ## The helpers The second-based helpers on a scheduled event are: - `everySecond()`, `everyTwoSeconds()`, `everyFiveSeconds()` - `everyTenSeconds()`, `everyFifteenSeconds()`, `everyTwentySeconds()`, `everyThirtySeconds()` Each calls a protected `repeatEvery($seconds)` that stores a repeat interval and then calls `everyMinute()`, so the task's cron expression is `* * * * *`. The interval must be greater than zero and divide 60 evenly — otherwise an `InvalidArgumentException` is thrown — and the public helpers only offer such divisors, so there is no seven-second helper. Because the expression is every-minute, chaining a day-level helper afterwards (for example `daily()`) confines the repetition to that single minute. ## What schedule:run does differently When at least one due event is repeatable, the command's behaviour changes: 1. It runs the normal pass: every due event once, in definition order. 2. Instead of exiting, it enters a loop that lasts until the **end of the minute in which the command started**. 3. Every 100 ms it walks the repeatable events and asks whether each one's interval has elapsed since it was last checked. 4. For an event that is ready it re-checks the pause flag, maintenance mode and the event's `when()`/`skip()` filters, then runs it again. 5. When the minute ends the process exits; cron has already started the next minute's `schedule:run`. Only events that were due at the start of the minute are repeated, and before the first pass the command clears any stale interrupt signal left in the cache. ## Pitfall 1: everything is sequential All of this happens in **one PHP process, one task at a time**. If a `Schedule::call()` closure scheduled `everyFiveSeconds()` takes eight seconds, every other sub-minute task waits behind it and the "every five seconds" promise quietly becomes "whenever there is a gap". The documentation's recommendation is to make the sub-minute task a dispatcher rather than the worker: | Definition | What happens each tick | | --- | --- | | `Schedule::call(fn () => $inventory->syncAll())->everyFiveSeconds()` | Runs the whole sync inline, blocking the loop | | `Schedule::job(new SyncInventory)->everyFiveSeconds()` | Pushes a queued job and returns immediately | | `Schedule::command('inventory:sync')->everyFiveSeconds()->runInBackground()` | Starts the command in the background and returns | With the queued version, the scheduler's timing stays accurate and the heavy work scales with queue workers. ## Pitfall 2: deploys and stale code A `schedule:run` started at 12:00:00 lives until about 12:00:59. If you deploy at 12:00:20, that process keeps executing the **previous release's** code and definitions for the rest of the minute. Laravel provides a `schedule:interrupt` command to add to deployment scripts, which signals in-progress runs to stop early; its mechanics belong with the scheduler's overlap and lifecycle controls. ## Pitfall 3: cost and expectations - Each minute still boots the full framework once, and the loop keeps a PHP process resident for the whole minute. - The timing is best effort: runs are measured from when the task was last checked, and a 100 ms poll adds jitter. - A foreground sub-minute task does not overlap itself inside one `schedule:run`, because runs are sequential — but a slow run that crosses the minute boundary can coincide with the first pass of the next minute's process. - If you need true continuous processing (consume events as they arrive), a queue worker or a long-running command is the better fit; sub-minute scheduling is for light periodic triggers. ## Locally and in review `php artisan schedule:work` starts a `schedule:run` subprocess at the top of every minute, so sub-minute tasks behave on a laptop exactly as they do under cron: each subprocess lives through its minute and repeats the task. In code review, three questions catch most problems with a new sub-minute task: 1. Does each tick finish comfortably inside its interval, even on a slow day? If not, make the tick dispatch a job. 2. Is the work safe to run a few seconds late, or twice in quick succession around a minute boundary? 3. Is there a deploy step that stops the old minute's process early, so new code takes effect promptly?

  • Why is there no everySevenSeconds() helper in Laravel's scheduler?
    The second-based helpers all go through a protected `repeatEvery()` that rejects any interval that does not divide 60 evenly, throwing an `InvalidArgumentException`. Seven seconds would drift across minute boundaries, and each `schedule:run` only covers its own minute, so only divisors of sixty are offered.
  • Does defining one everySecond() task change how long Laravel's schedule:run lives when no repeating task is due?
    No. The loop is entered only when at least one of the events due in that minute is repeatable. If every sub-minute task is filtered out as not due, for example by its environment or cron expression, `schedule:run` runs the normal pass and exits as usual.

saying these in an interview costs you the question

  • Sub-minute tasks need a second crontab entry that fires every second.
  • Laravel runs each sub-minute task in its own parallel process by default.
  • A slow sub-minute closure cannot delay the scheduler's other tasks.
  • everySecond() makes schedule:run a permanent daemon that never exits.
  • Any whole number of seconds is accepted as a repeat interval.