Skip to content
Skip to main content
A minimalist wall clock above a row of blank manila tags on brass hooks, a visual for the n8n durable scheduler recording each Schedule Trigger run in advance
9 min readBy Carlos Aragon

n8n Durable Scheduler: What Changes for Schedule Triggers

The n8n durable scheduler stores every upcoming Schedule Trigger run in the database, and from n8n 2.45it's on by default for every self-hosted instance. Three things change on upgrade day. A run that comes due during a quick restart now fires late instead of vanishing. "Every N minutes" schedules stop landing on the clock and run at a fixed gap from when the workflow went live. And after a longer outage, your old trigger nodes still skip what they missed, because only a v1.4 Schedule Trigger can be told to catch up. I ran all of it on n8n 2.44.3 to see what actually fires.

What Is the n8n Durable Scheduler?

Until now, n8n scheduled your workflows the way a kitchen timer does. The main process worked out when each active Schedule Trigger should fire and held that timer in memory. Restart the container and the timers die with it. Anything that came due while n8n was down simply never ran, and nothing logged that it was skipped.

The durable scheduler moves that into two tables. scheduled_job holds the rule (cron expression or interval, misfire policy, grace period). scheduled_taskholds the individual runs, written about a minute before they're due, then claimed, executed and marked succeeded, failed or missed. Because the queue lives in Postgres or SQLite, every main instance can claim from it, which is why it matters a lot if you run n8n in queue mode with multiple mains.

It isn't new code. It's a new default. It shipped as a preview in n8n 2.32, became a supported opt-in in 2.36, and the official durable scheduler docs now say it's on by default from 2.45.0 for new and existing instances. n8n's changelog dates 2.45 to October 20, 2026. As I write this, npm's stable tag is 2.43.7 and beta is 2.44.3, so you have a few days to get ready instead of finding out from a client.

What Changes When You Upgrade to n8n 2.45?

Here's the before and after for a single self-hosted instance:

SituationIn-memory (2.44 and older)Durable (2.45 default)
Run due while n8n restartsSkipped. The timer died with the process.Fires late if within the grace period (60s default).
Outage longer than the grace periodSkipped.Misfire policy decides. Default still skips.
Every N minutes / secondsAligned to the clock (:00, :15, :30…)Fixed gap from activation time
Hours, days, weeks, months, cronClock-alignedSame, with leap-year and DST edge cases fixed
Multi-mainLeader fires everythingEach main claims runs from a shared queue
Gmail, Sheets and other poll triggersIn memoryStill in memory unless you opt in

Existing Schedule Trigger nodes switch over without republishing. The per-node Skip Durable Scheduler setting and the N8N_ENV_FEAT_SKIP_DURABLE_SCHEDULER flag are gone in 2.45, so if you used them to keep one fussy workflow on the old timer, that escape hatch closes. One more prerequisite hides in the docs: the durable scheduler only takes over Schedule Triggers while N8N_USE_WORKFLOW_PUBLICATION_SERVICE is on. It is by default. If someone turned it off, n8n logs a warning and quietly keeps the old behavior.

What Happened When I Tested It on n8n 2.44.3

Docs describe intent. I wanted to see behavior. So I spun up a clean n8nio/n8n:2.44.3 container with SQLite and N8N_SCHEDULER_ENABLED=true, which is exactly what 2.45 does for you, and imported five workflows that do nothing but fire a No Op:

  • every 1 minute (Schedule Trigger v1.4)
  • every 45 seconds (v1.4)
  • cron every minute, v1.4, If Execution Is Missed set to Run the Most Recent Missed Execution
  • cron every minute, v1.4, default policy
  • cron every minute on an old v1.2 node, the version most of my production workflows use

Then I let it run, stopped the container twice with docker stop, and read the execution_entity and scheduled_task tables afterward. All times below are UTC.

TestWhat came dueWhat fired
Normal running, mode newEvery 1 min, every 45s, cron every minute1-min job at :21 (activation second), 45s job exactly 45s apart across minute lines, cron at :00
Container stopped 26 seconds5 runs came due while down or bootingAll 5, 18 to 39 seconds late, including the old v1.2 node
Container stopped 3 minutes17 runs came due1. The v1.4 node set to Run the Most Recent Missed Execution, once. The other 16 were dropped
Restarted with mode legacySame 1-min and 45s jobsRewritten as cron: 45s job at :00 and :45 (gaps of 45s, then 15s), 1-min job at second :04

The short restart is the headline win: zero lost runs. The container went down at 14:15:41 and came back at 14:16:07. Schedules started firing again at 14:16:39, so the 14:16:00 cron runs were 39 seconds late and still inside the 60-second grace. On the in-memory scheduler those runs are just gone. Note that the old v1.2 node got the benefit too. You don't need to touch a node to get restart safety.

The 3-minute outage is where people will be surprised. The scheduler had the 14:17:36 and 14:18:21 runs of the 45-second job sitting in scheduled_task before I pulled the plug, and after the restart it marked both missed. Not run, missed. Across all five workflows only one execution came out of the backlog: a single 14:20:00 run for the node set to catch up, fired at 14:20:52. Its default-policy twin and the v1.2 node skipped all three of their missed minutes and simply resumed at 14:21:00.

Two smaller details showed up that the docs don't spell out. First, the interval anchor is the moment the schedule was registered, and it survives restarts. My 1-minute job was registered at 14:12:21, so it fired at :21 every minute, before and after both outages. Second, the clock didn't start when I ran publish:workflow from the CLI at 14:11:48. It started when the running instance activated the workflow 33 seconds later.

Why Does My Every-15-Minutes Workflow No Longer Run on the Quarter Hour?

Because 2.45 also flips N8N_SCHEDULER_TRIGGER_NODE_MODE from legacy to new. That setting only touches "every N seconds" and "every N minutes" rules, and it changes what "every" means.

To see the old behavior side by side, I restarted the same container with N8N_SCHEDULER_TRIGGER_NODE_MODE=legacy. n8n rewrote both interval jobs as cron expressions in scheduled_job: every 45 seconds became */45 * * * * * and every 1 minute became 4 */1 * * * *. The 45-second job then fired at :45 and :00 of each minute, a 45-second gap followed by a 15-second one. That's legacy mode in one picture. It snaps to the clock even when the math doesn't fit, and an every-15-minutes rule lands on :00, :15, :30 and :45.

In newmode the gap is exact and the phase comes from activation. Publish an every-15-minutes workflow at 9:07:40 and it runs at 9:22:40, 9:37:40 and so on, forever, including after restarts. That's better engineering and worse for anyone whose downstream system expects data at :00, :15, :30 and :45. Think a report a client opens on the hour, a rate-limited API window, or two workflows that were deliberately offset by 5 minutes.

The fix I'd use is a cron rule, not the legacy flag. Switch the trigger's rule to Custom (Cron) and enter */15 * * * *. Cron expressions, plus hourly, daily, weekly and monthly rules, fire on the clock in both modes. Setting N8N_SCHEDULER_TRIGGER_NODE_MODE=legacy works too, but 2.45 deprecates it, logs a warning at every boot, and a later release removes it.

Will n8n Run the Executions It Missed While It Was Down?

Only the late ones, by default. A run counts as missed once it's later than its grace period, N8N_SCHEDULER_MISFIRE_GRACE, which defaults to 60 seconds. After that the trigger's misfire policy takes over, and there are three:

  • Don't Run Missed Executions (default). Drop the whole backlog and resume at the next slot. Same outcome as the old scheduler.
  • Run the Most Recent Missed Execution.Collapse the backlog into one run at the latest missed time. That's what fired at 14:20:52 in my test.
  • Run the Most Recent Missed Execution Per Rule. Same, but a trigger with three rules can fire three catch-up runs.

No policy replays a backlog run by run. A 3-hour outage on an hourly job gives you at most one catch-up, not three. If you need every slot processed, the workflow itself has to look back over the gap, and that only works if it's safe to run twice, which is the same discipline I lay out in my guide to n8n duplicate executions and idempotency.

Here's the catch nobody mentions: the policy and grace options only exist on Schedule Trigger typeVersion 1.4. I checked the node source in the 2.44.3 image. Both settings are gated on @version >= 1.4, and every older node gets skipwith the instance grace. When I ran the inventory script below against my agency's n8n, it found 46 Schedule Trigger nodes across 44 of 185 workflows. 37 are v1.2, six are v1.3, three are v1.1. Zero are v1.4. Not one of them can be told to catch up without deleting the trigger and adding a fresh one.

The grace period deserves a real number, too. My empty test container resumed firing about 32 seconds after docker start. A production box with a few hundred workflows, community nodes and a migration to run on upgrade day can easily take longer than 60 seconds, and then a routine restart counts as an outage. Time your own restart and set the grace above it.

How Do I Prepare My Instance Before Upgrading?

Budget 45 minutes. Treat it like any other n8n major-ish change, the way I handled the n8n 3 breaking changes: inventory first, change second.

1. Inventory every Schedule Trigger

This uses the public API and prints one line per rule. DRIFTS marks rules whose timing moves in 2.45; no-catch-upmarks nodes that can't use a misfire policy.

import json, os, urllib.request

req = urllib.request.Request(
    "https://YOUR-N8N/api/v1/workflows?limit=250",
    headers={"X-N8N-API-KEY": os.environ["N8N_API_KEY"]},
)
for wf in json.load(urllib.request.urlopen(req))["data"]:
    for node in wf["nodes"]:
        if node["type"] != "n8n-nodes-base.scheduleTrigger":
            continue
        for rule in node["parameters"].get("rule", {}).get("interval", [{}]):
            field = rule.get("field", "days")
            flag = "DRIFTS" if field in ("minutes", "seconds") else ""
            old = "no-catch-up" if node["typeVersion"] < 1.4 else ""
            print(wf["active"], wf["name"], node["typeVersion"], field, flag, old)

On my instance it flagged 16 minute-interval rules (six of them every 15 minutes). All 16 sit on inactive workflows today, which is luck, not planning. The eight active schedules are cron, hourly, daily or monthly, so they keep their times.

2. Pin clock-sensitive schedules to cron

Any active DRIFTS rule that something downstream depends on becomes a cron rule. Leave the rest alone. A polling job that just needs to run roughly every 10 minutes is happier with the new even spacing.

3. Decide which jobs should catch up

Invoice sends, nightly syncs and daily digests usually should. Anything time-sensitive, like an "event starts in 15 minutes" reminder, usually shouldn't. For the ones that should, replace the trigger with a fresh v1.4 Schedule Trigger and set If Execution Is Missed. Setting it on a v1.4 node in my test wrote coalesce_owner into scheduled_job.misfirePolicy on activation, so you can confirm it in the database.

4. Set the grace period and clear old flags

Raise N8N_SCHEDULER_MISFIRE_GRACE if your restarts are slow, remove N8N_ENV_FEAT_SKIP_DURABLE_SCHEDULER, and don't set N8N_USE_WORKFLOW_PUBLICATION_SERVICE=false. If you run on Docker Desktop for Mac, keep the database on a named volume rather than a bind mount; the scheduler now writes to it every few seconds, and bind-mounted databases on macOS have bitten me before.

5. Watch for missed runs after the next restart

Missed rows are kept for seven days (N8N_SCHEDULER_FAILED_RETENTION), so this query tells you exactly what an outage cost you:

SELECT j."ownerId" AS workflow_id, t."scheduledFor", t.status
FROM scheduled_task t
JOIN scheduled_job j ON j.id = t."jobId"
WHERE t.status = 'missed'
ORDER BY t."scheduledFor" DESC
LIMIT 50;

Missed runs don't trigger your error workflow, because nothing executed. If you rely on n8n error workflows for alerting, add a small scheduled check on this table instead.

Should You Turn the Durable Scheduler Off?

No. Turn on the parts you need and fix the few schedules that cared about the clock. N8N_SCHEDULER_ENABLED=falseexists, and it's a fine rollback for a bad upgrade night. But it's deprecated on arrival, and the thing it brings back is a scheduler that silently drops every run that comes due during a deploy. A 20-second container restart at the top of the hour is enough to lose an hourly client report on the old scheduler, and nothing anywhere tells you it happened.

Gmail, Outlook, Drive and Sheets triggers aren't part of this. They stay on the in-memory poller unless you set N8N_SCHEDULER_POLL_TRIGGERS_ENABLED, and n8n itself says that path isn't fully stable yet. My instance has 23 of those triggers, and they stay where they are.

On n8n Cloud there's nothing to configure. The trade-offs of running it yourself are in my n8n Cloud vs self-hosted comparison.

How I checked this: behavior is from a clean n8nio/n8n:2.44.3 Docker container on October 16, 2026 with N8N_SCHEDULER_ENABLED=true, SQLite, five No Op workflows and two timed docker stop outages, read straight from the execution_entity, scheduled_job and scheduled_tasktables. Option names and version gates come from the built ScheduleTrigger node in that image. The 2.45 defaults and the October 20 date come from n8n's docs and changelog, read the same day; 2.45 wasn't on npm yet. Production numbers are from my agency's self-hosted n8n via the public API.

n8n Durable Scheduler FAQ

What is the n8n durable scheduler?

It's n8n's database-backed scheduler for Schedule Trigger nodes. Instead of holding timers in memory, it writes each upcoming run to the scheduled_task table before it's due, so restarts don't drop runs and multiple main instances can share the work. It's opt-in from n8n 2.36 and on by default from n8n 2.45.

Does n8n run missed Schedule Trigger executions after a restart?

With the durable scheduler, runs that are late by less than the grace period (60 seconds by default) still fire. Past that, the default policy discards the backlog. Only Schedule Trigger nodes at typeVersion 1.4 can be set to run the most recent missed execution instead.

Why does my n8n every 15 minutes schedule no longer run on the quarter hour?

From n8n 2.45, 'every N minutes' and 'every N seconds' rules run at fixed intervals counted from when the workflow was activated, not from clock boundaries. Use a cron expression like */15 * * * * if the exact minute matters, or set N8N_SCHEDULER_TRIGGER_NODE_MODE=legacy as a temporary, deprecated fallback.

How do I turn off the durable scheduler in n8n?

Set N8N_SCHEDULER_ENABLED=false on every main instance and restart. It's deprecated in n8n 2.45 and logs a startup warning, and a future version removes the in-memory scheduler, so treat it as a short-term rollback.

Does the durable scheduler affect Gmail or Google Sheets triggers?

No. Poll triggers stay on the in-memory scheduler unless you opt them in with N8N_SCHEDULER_POLL_TRIGGERS_ENABLED, which n8n says isn't fully stable yet. Only Schedule Trigger and legacy Cron nodes move in 2.45.

Want Someone to Run the Upgrade With You?

I run self-hosted n8n for my own agency and for clients. If you'd rather not find your scheduling edge cases in production, send me your instance details and I'll audit the Schedule Triggers, pin what needs pinning and plan the 2.45 upgrade.

Related Posts