Automation running twice? It is overlapping itself
A scheduled automation processes the same record twice when the next run starts before the previous one has written back its result, which happens whenever run duration approaches the schedule interval. Platform settings that promise one run at a time are throttles rather than locks: n8n's concurrency limit applies to the whole instance, Make's sequential processing halts on any unresolved incomplete execution, and Zapier documents that its queue delay does not guarantee steps never run simultaneously. The reliable fix is to claim each record with a status write before doing the work, so an overlapping run finds nothing left to take.
When the same customer gets two emails, or an invoice posts twice, the cause is usually not a duplicate event from the source system. It is two copies of your own automation running at the same time, both reading the same records before either one wrote back that it had handled them. Every scheduled automation has a window between reading work and marking it done, and if a run takes longer than the interval between runs, that window is where your duplicates come from. The fix is not a longer interval and not the platform's "one at a time" switch. It is claiming each record before you work it.
A five-minute schedule does not mean runs are five minutes apart
Schedules fire on a clock. They do not wait for the last run to finish. So the real spacing between two runs touching the same data is the interval minus however long a run takes, and that second number is the one that moves.
Walk the sequence. Run 1 starts at 10:00 and reads twelve rows where status = new. It calls an enrichment API, which is slow this morning, and each row takes twenty seconds. At 10:04 it is on row 11. At 10:05, run 2 starts, reads status = new, and gets the same twelve rows, because run 1 has not written a single status back yet. Both runs now process the same eleven records. Neither one errors. Both execution logs are green.
This is why the bug is so hard to see in the platform UI. Nothing failed. You get a support ticket about a double email three days later and go looking for a retry that never happened.
The arithmetic worth memorizing: if the 95th-percentile run duration is more than half the interval, you are already overlapping regularly, and if it is more than the interval you are overlapping constantly. Not eventually. Now.
Two design habits make it much worse. The first is a high-water mark, meaning a "last synced at" timestamp that the run advances at the very end. Everything the run processed stays eligible until that final write lands, which turns the whole run duration into an overlap window. The second is pagination that walks a large result set, because a run that pages through 4,000 records is a run that stays alive long enough to meet its own successor.
The platform switch is a throttle, not a lock
Every major platform sells something that sounds like a lock. Read what each one actually does before you rely on it.
| Platform | The control | What it really does | Where it bites |
|---|---|---|---|
| n8n, self-hosted | N8N_CONCURRENCY_PRODUCTION_LIMIT | Caps how many production executions run at once and queues the rest. Disabled by default. | The limit is per instance, not per workflow. Setting it to 1 to protect one workflow throttles everything else on the box. It also excludes manual, sub-workflow, error, and CLI executions, so a test run can still race the scheduled one. |
| Make | Sequential processing, per scenario | Postpones the next run until the previous one finishes and until all of the scenario's incomplete executions are resolved. | One unresolved incomplete execution stops new runs entirely. You trade duplicates for a pipeline that quietly stops until somebody clears the queue. |
| Zapier | Delay After Queue | Queues runs and processes them one at a time with a delay between each. | Zapier's own documentation states it does not guarantee that following steps will never run simultaneously, and names infrastructure slowdowns and Zap run replays as reasons some steps may still run at the same time. |
Three different vendors, three honest descriptions, and not one of them is a mutual-exclusion lock on a record. They are back-pressure. Back-pressure is genuinely useful, especially for protecting a rate-limited downstream API, and I turn it on. I just never let it be the only thing standing between a customer and a second invoice.
There is a fourth reason not to lean on serialization: it does not survive the replay. Any manual re-run, any auto-retry after an error, any re-import of yesterday's file bypasses the schedule entirely, and a control that only orders scheduled runs has nothing to say about it.
Claim the record before you work it
The pattern that holds is small. Instead of preventing two runs from existing, make the second run find nothing to take.
Add three fields to the records you process, or to a control table beside them if you cannot alter the source: a status, a claimed_at timestamp, and a claimed_by value holding the execution ID of the run that took it.
Then reorder your workflow so the write comes before the work.
Read a bounded batch. Select rows where status is unclaimed, with an explicit limit. Fifty, a hundred, whatever one run can finish comfortably. Unbounded reads are how runs get long enough to overlap in the first place.
Write the claim immediately. Set status to claimed, claimed_by to this run's execution ID, and claimed_at to now. This is the first thing the run does after reading, before any API call, any AI step, any transformation.
Confirm what you actually own. In a real database, make the claim a single conditional update and let the engine resolve the race. In Postgres that is UPDATE jobs SET status='claimed', claimed_by=$1 WHERE id IN (SELECT id FROM jobs WHERE status='new' LIMIT 50 FOR UPDATE SKIP LOCKED) RETURNING id, and the IDs it returns are yours alone. In Airtable, Sheets, or a CRM with no conditional write, claim and then read back only the rows carrying your run ID. Rows another run grabbed first come back with a different ID and fall out of your batch.
Work only the confirmed set, then mark it done. Status goes to done or failed per record, not per run, so a partial run leaves accurate state behind instead of an all-or-nothing guess.
Reap stale claims on a separate schedule. A run that dies after claiming leaves rows stuck. A small job that resets rows claimed longer ago than your maximum run duration, and logs every reset, fixes that. Set the threshold from real data, not optimism: take your slowest observed run and double it.
The result is that overlap becomes harmless rather than impossible. Run 2 starts early, finds every eligible row already claimed, processes nothing, and exits clean. That is a much better failure mode than a lock that jams.
This is the same idea as the idempotency key that stops an automation creating duplicates, applied one layer earlier. The idempotency key stops the second write from landing. The claim stops the second run from doing the work at all, which also saves you the API calls and the AI tokens the duplicate would have burned.
What overlap breaks, beyond duplicate records
Duplicates are the symptom people notice. These are the ones they misdiagnose.
Two runs updating the same record produce a last-writer-wins collision where the winner may hold the older data, which looks exactly like a two-way sync overwriting good values and sends people hunting in the wrong system.
Concurrent runs double your call rate against a downstream API, so a workflow that fits comfortably inside a rate limit at one run starts collecting 429s at two, and the retries then extend run duration, which widens the overlap window further. That loop is self-feeding.
Counters and totals drift, because increments applied by two runs both land. Nothing errors, the number is just wrong, and it stays wrong.
Attribution in your logs gets scrambled: run A marks a record complete while run B is still working it, so B's eventual error attaches to a record that already reads as done. If your logs do not carry the execution ID on every line, this is nearly impossible to untangle after the fact, which is one more argument for logging the run identity on every write.
Check your own workflows in ten minutes
Open the execution history of your busiest scheduled workflow and put the start times and durations side by side. Sort by start time and look for any run whose start falls before the previous run's end. One overlap in the last hundred runs means the pattern exists and will get worse as volume grows.
Then compare the slowest recent run against the schedule interval. If your interval is five minutes and your slowest run is three, you are one bad API morning from processing everything twice.
Last, read the first two steps of the workflow and answer one question: between the read and the first status write, how many steps are there? If the answer is more than zero, that is your window, measured in whatever those steps cost. Moving the status write to position one is usually thirty minutes of work and it closes most of the gap by itself.
If several workflows share the same source table, do this once per workflow and then check whether two different workflows are claiming from the same pool, because that is the version of this bug that no amount of per-workflow serialization can fix. We run this check as part of any workflow reliability audit, and it is the single most common finding in automations that have been running quietly for a year. If you want a second read on where your runs are colliding, send us the execution history and the step list and we will point at the window.
Frequently Asked Questions
SOURCES & CITATIONS
- Control concurrency, n8n hosting docs — n8nhttps://docs.n8n.io/deploy/host-n8n/configure-n8n/scaling/control-concurrency/
- Scenario settings — Make Help Centerhttps://help.make.com/scenario-settings
- Incomplete executions — Make Help Centerhttps://help.make.com/incomplete-executions
- Add delays to Zap workflows — Zapierhttps://help.zapier.com/hc/en-us/articles/8496288754829-Add-delays-to-Zaps
About Alexey Yushkin
Alexey is the founder of GENERAL INFORMATICS LLC. He designs and ships AI and automation systems for businesses and operators across the US.
Related reading
Want this kind of system in your business?
We build practical AI and automation systems for operators. Send us your current workflow and we will show you what to automate first.
