What versions & operating system are you using?
Output collected on 2026-09-22 with the requested command:
npx envinfo --system --npmPackages '{wrangler,create-cloudflare,miniflare,@cloudflare/*}' --binaries
System:
OS: macOS 27.0
CPU: (10) arm64 Apple M1 Pro
Memory: 72.38 MB / 16.00 GB
Shell: 5.9 - /bin/zsh
Binaries:
Node: 26.9.0 - /opt/homebrew/bin/node
Yarn: 1.22.22 - /opt/homebrew/bin/yarn
npm: 12.0.2 - /opt/homebrew/bin/npm
pnpm: 12.5.1 - /opt/homebrew/bin/pnpm
bun: 1.4.2 - /opt/homebrew/bin/bun
The command emitted no npmPackages section in this npm workspace checkout.
Installed versions were checked separately with npm ls --json:
- Wrangler
4.135.0; @cloudflare/vitest-plugin 1.1.13;
@cloudflare/workers-types 5.20260920.1.
- Miniflare
5.20260918.0-alpha; workerd 1.20260918.1.
create-cloudflare is not installed in this checkout.
The standalone reproduction uses the direct Miniflare API, without Wrangler,
Vitest, remote bindings or deployment. It also failed with stable Miniflare
4.20260730.0; that separate installation is not part of the checkout report.
System requirements checked on the same date: macOS 27.0 meets the documented
macOS 13.5+ minimum. Node.js 26 is a Current release, which Wrangler explicitly
supports alongside Active and Maintenance releases. This checks the host
requirements, not whether a particular dependency release is defect-free.
Please provide a link to a minimal reproduction
https://gist.github.com/alexminza/08cc651f71a29422efa49598b37ba01e
Describe the Bug
A local Workflow running sequential, immediately successful step.do() calls
hits the active-timeout quota after 5,000 completed steps. Each step has a unique
deterministic name, a ten-minute timeout and retries disabled. There is only one
Workflow instance and one step running at a time.
Expected: all 9,000 steps complete; timeout timers belonging to completed
steps are cancelled instead of accumulating until their original deadlines.
Actual: execution stops at 5,000 steps with QuotaExceededError reporting
10,000 active timeouts. This is not a slow step reaching its timeout.
Steps to reproduce
- Download the gist's
miniflare-workflow-timers.mjs into an empty directory.
- Follow the header comment's install and run instructions to request 9,000 steps.
- Inspect
output.completed and output.error. The Workflow catches the
exception to report progress, so terminal status: "complete" and exit code
zero do not mean that all requested steps completed.
Verified cause
The installed source-map contents of context.ts and
lib/gracePeriodSemaphore.ts matched the corresponding upstream files at
miniflare@5.20260918.0-alpha exactly.
In packages/workflows-shared/src/context.ts, timeoutPromise() starts
scheduler.wait(timeout) without an abort signal. Successful completion aborts
the existing controller and calls TimePriorityQueue.remove(), but that only
updates the heap and SQLite bookkeeping. Neither operation cancels the scheduled
wait, and winning Promise.race() does not cancel its losing operation.
In the installed workerd version, Scheduler::wait() connects cancellation to
clearTimeoutImpl() only when supplied an abort signal. The timeout manager
enforces a limit on active timers, not on the cumulative number of steps.
Diagnostic comparison
A disposable copy of Miniflare 5.20260918.0-alpha was changed only as follows:
- await scheduler.wait(timeout);
+ await scheduler.wait(timeout, { signal: abortController.signal });
| Runtime |
Completed / requested steps |
Result |
Stable 4.20260730.0, unmodified |
5,000 / 9,000 |
Active-timeout quota error |
5.20260918.0-alpha, unmodified |
5,000 / 9,000 |
Same error |
| Same alpha, experimental timeout cancellation |
9,000 / 9,000 |
No error |
| Same patch, extended no-op run |
12,000 / 12,000 |
No error |
The alpha baseline and patched comparison were rerun while preparing this draft
with the same results. The stable row is from the earlier isolated run on the
same date, 2026-09-22. The extended run set stepLimit: 20000 on workflows.TEST
in the reproducer's options; it did not change the active-timer quota. Without
that override, the extended run stopped at the separate 10,000-step limit, not
with a timer-quota error.
One follow-up with the same override completed 10,001 steps when only the last
step awaited scheduler.wait(1) before returning. A 12,000-step variant awaiting
that delay in every step exceeded the runner's one-minute budget and is
inconclusive, not another reproduced timer-quota failure.
Limits of the diagnosis
packages/workflows-shared/src/lib/gracePeriodSemaphore.ts also starts a wait
without cancellation: the next step invalidates its timestamp, not its timer.
This is a source-level concern, not an independently reproduced second
failure. The extended patched runs passed; the exact contribution of these
waits to quota exhaustion has not been isolated.
The one-line experiment supports the step-timeout diagnosis, not a fully
validated fix. Actual timeout expiry, retry, pause/resume and restart semantics
remain untested with it. Source inspection of 5.20260921.0-alpha found the same
uncancelled waits, but that version was not runtime-tested here. No patch has
been retained in the application, and no deployed Cloudflare Workflows failure
has been demonstrated.
References
Please provide any relevant error logs
Unmodified baseline output on 2026-09-22, omitting only the elapsed-time field:
{
"steps": 9000,
"status": "complete",
"output": {
"completed": 5000,
"error": {
"name": "QuotaExceededError",
"message": "You have exceeded the number of active timeouts you may set. max active timeouts: 10000, current active timeouts: 10000, finished timeouts: 0"
}
}
}
What versions & operating system are you using?
Output collected on 2026-09-22 with the requested command:
npx envinfo --system --npmPackages '{wrangler,create-cloudflare,miniflare,@cloudflare/*}' --binariesThe command emitted no
npmPackagessection in this npm workspace checkout.Installed versions were checked separately with
npm ls --json:4.135.0;@cloudflare/vitest-plugin1.1.13;@cloudflare/workers-types5.20260920.1.5.20260918.0-alpha; workerd1.20260918.1.create-cloudflareis not installed in this checkout.The standalone reproduction uses the direct Miniflare API, without Wrangler,
Vitest, remote bindings or deployment. It also failed with stable Miniflare
4.20260730.0; that separate installation is not part of the checkout report.System requirements checked on the same date: macOS 27.0 meets the documented
macOS 13.5+ minimum. Node.js 26 is a Current release, which Wrangler explicitly
supports alongside Active and Maintenance releases. This checks the host
requirements, not whether a particular dependency release is defect-free.
Please provide a link to a minimal reproduction
https://gist.github.com/alexminza/08cc651f71a29422efa49598b37ba01e
Describe the Bug
A local Workflow running sequential, immediately successful
step.do()callshits the active-timeout quota after 5,000 completed steps. Each step has a unique
deterministic name, a ten-minute timeout and retries disabled. There is only one
Workflow instance and one step running at a time.
Expected: all 9,000 steps complete; timeout timers belonging to completed
steps are cancelled instead of accumulating until their original deadlines.
Actual: execution stops at 5,000 steps with
QuotaExceededErrorreporting10,000 active timeouts. This is not a slow step reaching its timeout.
Steps to reproduce
miniflare-workflow-timers.mjsinto an empty directory.output.completedandoutput.error. The Workflow catches theexception to report progress, so terminal
status: "complete"and exit codezero do not mean that all requested steps completed.
Verified cause
The installed source-map contents of
context.tsandlib/gracePeriodSemaphore.tsmatched the corresponding upstream files atminiflare@5.20260918.0-alphaexactly.In
packages/workflows-shared/src/context.ts,timeoutPromise()startsscheduler.wait(timeout)without an abort signal. Successful completion abortsthe existing controller and calls
TimePriorityQueue.remove(), but that onlyupdates the heap and SQLite bookkeeping. Neither operation cancels the scheduled
wait, and winning
Promise.race()does not cancel its losing operation.In the installed workerd version,
Scheduler::wait()connects cancellation toclearTimeoutImpl()only when supplied an abort signal. The timeout managerenforces a limit on active timers, not on the cumulative number of steps.
Diagnostic comparison
A disposable copy of Miniflare
5.20260918.0-alphawas changed only as follows:4.20260730.0, unmodified5.20260918.0-alpha, unmodifiedThe alpha baseline and patched comparison were rerun while preparing this draft
with the same results. The stable row is from the earlier isolated run on the
same date, 2026-09-22. The extended run set
stepLimit: 20000onworkflows.TESTin the reproducer's options; it did not change the active-timer quota. Without
that override, the extended run stopped at the separate 10,000-step limit, not
with a timer-quota error.
One follow-up with the same override completed 10,001 steps when only the last
step awaited
scheduler.wait(1)before returning. A 12,000-step variant awaitingthat delay in every step exceeded the runner's one-minute budget and is
inconclusive, not another reproduced timer-quota failure.
Limits of the diagnosis
packages/workflows-shared/src/lib/gracePeriodSemaphore.tsalso starts a waitwithout cancellation: the next step invalidates its timestamp, not its timer.
This is a source-level concern, not an independently reproduced second
failure. The extended patched runs passed; the exact contribution of these
waits to quota exhaustion has not been isolated.
The one-line experiment supports the step-timeout diagnosis, not a fully
validated fix. Actual timeout expiry, retry, pause/resume and restart semantics
remain untested with it. Source inspection of
5.20260921.0-alphafound the sameuncancelled waits, but that version was not runtime-tested here. No patch has
been retained in the application, and no deployed Cloudflare Workflows failure
has been demonstrated.
References
miniflare@4.20260730.0and reproduced alphaminiflare@5.20260918.0-alpha— the timeout wait and completion-time abort are not connected by a signal.remove()changes bookkeeping, not native timers.v1.20260918.1—Scheduler::wait()attaches native timer cleanup when a signal is supplied; timer manager and quota constant define active-timer accounting and the observed limit.miniflare@5.20260921.0-alphaand grace-period implementation at that tag — source-only follow-up, not another runtime result.Please provide any relevant error logs
Unmodified baseline output on 2026-09-22, omitting only the elapsed-time field:
{ "steps": 9000, "status": "complete", "output": { "completed": 5000, "error": { "name": "QuotaExceededError", "message": "You have exceeded the number of active timeouts you may set. max active timeouts: 10000, current active timeouts: 10000, finished timeouts: 0" } } }