skip to content

Overlap & One-Server Runs

withoutOverlapping and onOneServer take cache locks so a slow run or a second server never starts a duplicate, and hooks report output and failures. Interviewers probe stale locks and silent misses.

on this pageshow

explore

questions

6

In Laravel's scheduler, what does withoutOverlapping() do, where is its lock stored, and how long does that lock last by default?

level: middleimportance: must knowfreq 55%

answer

  1. by default runs pile up
  2. a cache lock named per task
  3. expression plus command in the key
  4. 1440 minutes unless you pass one
  5. released when the run finishes

basics

~20 s

withoutOverlapping() makes the scheduler take a cache lock before running a task and skip the task while that lock exists, so a slow run is not joined by a second copy. The lock expires after 1440 minutes unless you pass another value.

solid answer

~50 s

By default Laravel starts a task whenever it is due, even if the previous run is still going. `->withoutOverlapping()` adds a skip filter plus a lock: before running, the scheduler tries to create a mutex in the cache named after the task — by default a hash of its cron expression and command — and skips the run if it cannot. The lock is stored in the default cache store (or the one set with `Schedule::useCache()`), with a TTL of `expiresAt` minutes, **1440 (24 hours)** by default; `->withoutOverlapping(10)` makes it ten minutes. It is released when the run finishes or throws, and for foreground tasks also when `schedule:run` receives SIGTERM, SIGINT or SIGQUIT and the `pcntl` extension is loaded. Closures need `->name()` first, and on `Schedule::job()` the lock covers only the dispatch, not the job running on a worker.

code

php · 15 lines
php
<?php

use App\Services\ReportBuilder;
use Illuminate\Support\Facades\Schedule;

// Lock expires after 15 minutes instead of the default 1440
Schedule::command('reports:generate')
    ->everyFiveMinutes()
    ->withoutOverlapping(15);

// A closure must be named before it can be locked
Schedule::call(fn (ReportBuilder $reports) => $reports->refreshTotals())
    ->name('reports:refresh-totals')
    ->everyMinute()
    ->withoutOverlapping(5);

go deeper

for a junior

Know that tasks overlap by default and that withoutOverlapping() skips a run while the previous one holds its lock.

for a middle

Explain the cache mutex: the name from expression plus command, the TTL in minutes defaulting to 1440, and release on finish, exception or a trapped signal.

for a senior

Set expiries near the worst run time, share the cache store across scheduler hosts, name locked closures, and know the lock only guards dispatch for queued jobs.

for a principal

Decide which duplicate-run guarantees belong in the scheduler, the queue, or the task's own idempotency, rather than stacking locks everywhere.

## The problem: runs pile up Laravel's scheduler has no memory of previous runs. If `reports:generate` is scheduled `everyFiveMinutes()` and one run takes seven minutes, the next `schedule:run` starts a second copy while the first is still writing. Two copies of a report generator can duplicate rows, fight over files, or double the database load exactly when the system is already slow. `withoutOverlapping()` is the per-task guard against that. ```php use Illuminate\Support\Facades\Schedule; Schedule::command('reports:generate') ->everyFiveMinutes() ->withoutOverlapping(15); ``` ## How the lock works Calling `withoutOverlapping($expiresAt = 1440, $releaseOnTerminationSignals = true)` does three things to the event: 1. Sets its `withoutOverlapping` flag and its `expiresAt` value in **minutes**. 2. Adds a `skip()` filter that asks the event mutex whether the lock already exists; if it does, the run is skipped and `ScheduledTaskSkipped` is dispatched. 3. At run time, tries to **create** the mutex; if another process created it first, the run is skipped. The default event mutex, `CacheEventMutex`, uses the cache: - If the store supports atomic locks (it implements `LockProvider`, which database, Redis, Memcached and file stores do — DynamoDB is deliberately handled the other way), it acquires a cache lock with a TTL of `expiresAt * 60` seconds. - Otherwise it falls back to `Cache::add()` of a key with the same TTL. The **mutex name** is `framework/schedule-` plus a SHA-1 of the task's cron expression and normalised command. Two consequences: the same command scheduled at two different frequencies gets two different locks, so they can overlap each other; and a closure has no command string, so `Schedule::call()` must be given `->name('...')` before `withoutOverlapping()`, or a `LogicException` is thrown. ## When the lock is released | Situation | Lock released? | | --- | --- | | Run completes (any exit code) | Yes, after the after-hooks run | | Run throws before or during start | Yes | | Foreground task, `schedule:run` gets SIGTERM/SIGINT/SIGQUIT, `pcntl` loaded | Yes, then the process exits | | Background task (`runInBackground()`) | When the background command reports completion | | Only the child command process is killed | Yes — `schedule:run` sees the non-zero exit code and finishes normally | | `schedule:run` itself killed with SIGKILL, or the host dies | **No** — only when the TTL expires | The default 1440-minute TTL is a safety net for that last row: a crashed run blocks the task for up to a day. Pick an expiry comfortably above the longest normal run — `withoutOverlapping(15)` for a job that takes at most seven — so a crash costs minutes, not a day. ## Which cache, and which servers The lock lives in the scheduler's cache store: the default store unless `Schedule::useCache('redis')` (or the console kernel's `cache.schedule_store` config / `SCHEDULE_CACHE_STORE` env) points elsewhere. With a store local to one machine (`file`, `array`), the guard only stops overlaps on that machine. Stopping the same task from running on several servers in the same minute is a different option, `onOneServer()`; the two are often combined. ## What it does not cover - **Queued jobs.** `Schedule::job(new BuildReport)->withoutOverlapping()` guards only the moment of dispatch, which takes milliseconds. The job itself runs later on a worker, where overlap is controlled by queue-level tools such as unique jobs or job middleware. - **Manual runs.** `php artisan reports:generate` typed by hand does not take the scheduler's lock. - **Sibling tasks.** Two different commands touching the same table are not protected from each other. ## Checklist - Set an explicit expiry close to, but above, the worst realistic run time. - Name every closure you protect. - Use a shared cache store when more than one host runs the scheduler. - Watch `ScheduledTaskSkipped`: a task that skips every minute is usually holding a stale lock. ## Why interviewers ask it The option is one line of code, so the question is really about the model underneath: the scheduler is stateless between minutes, so any memory of a running task must live outside the process, in the cache. Candidates who see that can predict the rest — why the store matters across hosts, why a lock needs an expiry, why a closure needs a name to be locked, and why a queued job escapes the lock entirely.

  • Why can the same Laravel command scheduled hourly and daily overlap even when both use withoutOverlapping()?
    The default mutex name is a hash of the cron expression plus the command string, so `0 * * * *` and `0 0 * * *` versions of `reports:generate` hold different locks. At midnight both are due and neither sees the other's lock. Give them a shared lock name with `createMutexNameUsing()`, or merge them into one task.
  • Does withoutOverlapping() on Schedule::job in Laravel stop two copies of the job running on queue workers?
    No. The scheduled event only dispatches the job, so the lock is taken and released around a dispatch that takes milliseconds. If the previous job is still running on a worker, the next tick dispatches another. Overlap between queued jobs is controlled on the queue side, with unique jobs or job middleware.

saying these in an interview costs you the question

  • Laravel never starts a task while its previous run is still going, even without extra options.
  • withoutOverlapping() tracks running tasks by process ID in memory.
  • The default lock expiry for withoutOverlapping() is one minute.
  • withoutOverlapping() on a scheduled closure works without calling name().
  • withoutOverlapping() on Schedule::job prevents two copies running on workers.
open as a page

When a Laravel app scaled to three servers runs schedule:run on each, why does a report task run three times, and how does onOneServer() prevent it?

level: seniorimportance: must knowfreq 48%

basics

~20 s

Each server's cron runs schedule:run, and each sees the same task due, so it runs once per server. onOneServer() makes each server try a lock in a shared cache keyed by task and minute; only the winner runs it.

open as a page

In Laravel's scheduler, do tasks due in the same minute run in parallel, and what changes when you add runInBackground() to one?

level: juniorimportance: should knowfreq 40%

basics

~10 s

No: schedule:run runs due tasks one after another, in the order they are defined. runInBackground() starts a command or exec task as a detached process and moves on at once; closures cannot use it.

open as a page

How do you capture a Laravel scheduled command's output and get alerted when it fails, using sendOutputTo(), onFailure() and thenPing()?

level: middleimportance: should knowfreq 40%

basics

~20 s

Output of scheduled commands is discarded unless you chain sendOutputTo() or appendOutputTo(). onSuccess()/onFailure() run on exit code zero or non-zero and can receive the output as a Stringable; thenPing() and pingOnFailure() hit URLs for external monitors.

open as a page

A Laravel task using withoutOverlapping() stopped running after its scheduler container was force-killed mid-run; why, and how do you recover and prevent it?

level: seniorimportance: should knowfreq 34%

basics

~20 s

The schedule:run process died before it could delete the task's overlap lock, so every later tick finds the lock and skips the task until its TTL, 24 hours by default, expires. Clear it with schedule:clear-cache, then set a shorter expiry and monitor.

open as a page

During a Laravel deploy that uses php artisan down, what happens to scheduled tasks, and what do evenInMaintenanceMode() and schedule:interrupt change?

level: seniorimportance: nice to knowfreq 22%

basics

~20 s

While the app is in maintenance mode, scheduled tasks are treated as not due and silently do not run, unless marked evenInMaintenanceMode(). schedule:interrupt tells an already running sub-minute schedule:run loop to stop early, so it does not keep executing the previous release.

open as a page