In Laravel's scheduler, which timezone decides when dailyAt('02:00') fires, and how do the timezone() method and app.schedule_timezone change it?
answer
- skeleton app.timezone is UTC
- schedule_timezone falls back to app.timezone
- timezone() per task wins
- wall-clock match in that zone
- DST: skipped or doubled runs
basics
~20 sA task's time is matched against the clock in its timezone: the task's own timezone() if set, otherwise config app.schedule_timezone, otherwise app.timezone, which the skeleton sets to UTC. The server's operating-system timezone plays no part.
solid answer
~40 sWhen the console kernel builds the `Schedule` it passes `config('app.schedule_timezone')`, falling back to `config('app.timezone')` — and the skeleton's `config/app.php` hard-codes `'timezone' => 'UTC'`. Every task inherits that zone unless you chain `->timezone('Europe/Berlin')`. At each run the scheduler converts the current time into the task's zone and matches the cron expression against that local wall clock, so `dailyAt('02:00')` means 02:00 UTC by default. Adding `'schedule_timezone' => 'America/Chicago'` shifts every task without changing how dates are handled elsewhere in the app, which changing `app.timezone` would. In zones with daylight saving time a local time can be skipped or repeated, so a task may not run or may run twice on transition nights; the docs recommend avoiding timezone scheduling where possible.
code
php · 7 lines<?php
// config/app.php (excerpt)
return [
'timezone' => 'UTC', // dates app-wide stay UTC
'schedule_timezone' => 'Europe/Berlin', // default for every scheduled task
];go deeper
Remember the default: tasks run on app.timezone, which the skeleton sets to UTC, and timezone() changes it per task.
Explain the precedence of timezone(), app.schedule_timezone and app.timezone, and that the check matches the cron expression on the converted local wall clock.
Keep business-local tasks off DST transition hours, prefer schedule_timezone over changing app.timezone, and make time-sensitive tasks safe to double-run or catch up.
Set a policy of UTC by default with explicit, reviewed exceptions for local-time business rules, and account for regions whose DST rules differ.
## Three layers decide the clock Laravel's scheduler never looks at the server's operating-system timezone. The zone a task's cron expression is matched in comes from three settings, most specific first: 1. **The task's own `timezone()`** — `->timezone('Europe/Berlin')` accepts a timezone string, a `DateTimeZone`, or a backed enum whose value is a zone name. 2. **`app.schedule_timezone`** — an optional key you add to `config/app.php`. It is not in the skeleton's config file; the framework reads it if present. 3. **`app.timezone`** — the application's timezone. The Laravel 13 skeleton sets `'timezone' => 'UTC'` in `config/app.php`. The framework's console kernel constructs the `Schedule` with `config('app.schedule_timezone', config('app.timezone'))`, and every event created from it inherits that value until a `timezone()` call overrides it. ## How a task is matched Each minute, `schedule:run` asks every event whether its expression is due. The check takes the current time, **converts it into the event's timezone**, and tests the cron expression against the resulting wall-clock date and time. So in a default app: - `Schedule::command('subscriptions:renew')->dailyAt('02:00')` runs at 02:00 **UTC**. - `->dailyAt('02:00')->timezone('America/New_York')` runs at 02:00 New York time, which is 06:00 or 07:00 UTC depending on the season. - `between('8:00', '17:00')` evaluates its window in the task's timezone too. Chain order does not matter for `timezone()`: it sets a property the check reads at run time, not a cron field. ## schedule_timezone versus app.timezone | Setting changed | Effect on the schedule | Effect on the rest of the app | | --- | --- | --- | | `->timezone(...)` on one task | That task only | None | | `app.schedule_timezone` | Every task without its own `timezone()` | None | | `app.timezone` | Every task, when no `schedule_timezone` is set | Default zone for dates across the app | The classic mistake is changing `app.timezone` to local time "so the reports run at the right hour". That also changes the default timezone PHP dates use across the app, which is usually unwanted when timestamps are meant to stay in UTC. `schedule_timezone` exists precisely to decouple the two. One display detail: `php artisan schedule:list` converts each expression into `app.timezone` for display — not `schedule_timezone` — unless you pass `--timezone=Europe/Berlin`. Read its output with that in mind. ## Daylight saving time Matching against a local wall clock means DST transitions leak into the schedule. Laravel's documentation warns that a task in a DST-observing zone "may run twice or even not run at all": - **Spring forward** — a local time inside the skipped hour never occurs, so a task set for it does not run that night. - **Fall back** — a local time inside the repeated hour occurs twice, so the task can run twice. Practical guidance for a renewal job: - Keep tasks in UTC unless the business rule is genuinely local ("send at 9am shop time"). - If it must be local, choose a time outside the transition hour for that zone. - Make the task itself safe to repeat and able to catch up, so one doubled or missed night does no harm. ## Traps interviewers listen for - **"The server is in Berlin, so it runs at Berlin time."** The OS zone is irrelevant; only the three settings above count. - **"I'll just set `app.timezone` to local."** It works for the schedule but also changes the default zone of every date the app creates, which then disagrees with UTC timestamps already stored. - **"`timezone()` must come before `dailyAt()`."** Order does not matter; the zone is read when the expression is checked. - **"Laravel handles DST for me."** It matches wall-clock time and does nothing special on transition nights. - **"schedule:list shows the time the task runs."** It shows the expression converted into the display zone, which defaults to `app.timezone`. A quick sanity check before a release is `php artisan schedule:list --timezone=UTC` next to `--timezone=Europe/Berlin`: seeing both views makes an unintended offset obvious. The general theory of scheduling across clock changes belongs to distributed-cron design; the Laravel-specific fact is simply that matching happens on the converted local wall clock.
- A Laravel app sets schedule_timezone to Europe/Berlin but schedule:list shows the renewal task at a different hour. Why?`schedule:list` converts expressions into `app.timezone` for display, not into `schedule_timezone`. With `app.timezone` left at UTC, a 02:00 Berlin task is listed at its UTC equivalent. Pass `--timezone=Europe/Berlin` to see it in local time; the task itself still fires at 02:00 Berlin time.
- Does the server's operating-system timezone affect when a Laravel scheduled task runs?No. The scheduler converts the current instant into the task's configured timezone — `timezone()`, then `app.schedule_timezone`, then `app.timezone` — and matches against that. The OS zone would only matter indirectly, for example if the machine's clock itself were wrong.
saying these in an interview costs you the question
- Laravel matches scheduled tasks against the server's operating-system timezone.
- Changing app.timezone is the only way to schedule tasks in local time.
- The order of timezone() and dailyAt() in a chain changes the run time.
- Laravel automatically adjusts a local-time task so DST never skips or repeats it.
- schedule:list always displays times in app.schedule_timezone.