Skip to content

[miniflare] Enqueue undelayed queue messages without a timer - #15676

Open
L4XB wants to merge 6 commits into
cloudflare:mainfrom
L4XB:fix/15670-queues-zero-delay-timer-leak
Open

L4XB wants to merge 6 commits into
cloudflare:mainfrom
L4XB:fix/15670-queues-zero-delay-timer-leak

Conversation

@L4XB

@L4XB L4XB commented Sep 16, 2026 •

Copy link
Copy Markdown

Fixes #15670.

The local Queues broker hands every incoming message to a timer, even when the
message has no delivery delay:

// packages/miniflare/src/workers/queues/broker.worker.ts, #enqueue
const delay = message.delaySecs ?? globalDelay;
this.timers.setTimeout(fn, delay * 1000);

workerd caps a Durable Object at 10000 active timeouts, and none of those timers
run while the producer is still sending — the reported error says
finished timeouts: 0, so not one of the 10000 had completed. A Worker that
enqueues more messages than that in a single burst therefore fails:

QuotaExceededError: You have exceeded the number of active timeouts you may set.
max active timeouts: 10000, current active timeouts: 10000, finished timeouts: 0
    at Timers.setTimeout (miniflare/src/workers/shared/timers.worker.ts:30:11)
    at #enqueue (miniflare/src/workers/queues/broker.worker.ts:444:16)
    at QueueBrokerObject.batch (miniflare/src/workers/queues/broker.worker.ts:562:8)

The timer was introduced with delayed delivery in #5570; before that, #enqueue
pushed straight onto #messages. This change restores that for the undelayed
case and keeps the timer only where a delay was actually requested.

It also makes the real-timer path agree with the fake-timer path. In
Timers.setTimeout, a zero delay under fake timers goes to queueMicrotask,
whose fake implementation invokes the closure synchronously — so under fake
timers the broker has always enqueued undelayed messages immediately. Every
existing test in test/plugins/queues/ except cross-process.spec.ts enables
fake timers, which is why the suite never covered this.

The retry path in #flush also uses this.timers.setTimeout with a
possibly-zero delay, but it is bounded by construction — a retry can only be
scheduled by a flush, and a flush is itself a timer callback — so it is left
alone here.

Measurements

Sending batches of 100 through sendBatch() sequentially from one fetch()
handler, against packages/miniflare with dist rebuilt on each side:

messages before after
10 000 204, all delivered 204, all delivered
10 100 500, QuotaExceededError 204, all delivered
20 000 500, QuotaExceededError 204, all delivered
100 000 not attempted 204, all delivered in 1.2 s

The boundary sits exactly on workerd's limit: 10000 messages pass, 10001 fail.
That is the arithmetic signature of one live timer per message, and it is what
the new regression test pins — it enqueues 20000 messages, twice the ceiling, so
it can only pass if the broker's live timer count stays bounded well below the
number of messages in flight.

enqueues more undelayed messages than workerd's active timeout limit in
packages/miniflare/test/plugins/queues/index.spec.ts, run in a worktree at the
merge base (9515011) with only the test applied:

FAIL  test/plugins/queues/index.spec.ts > enqueues more undelayed messages than workerd's active timeout limit
AssertionError: Error: Queue sendBatch failed: Internal Server Error: expected 500 to be 204

Unhandled Rejection
QuotaExceededError: You have exceeded the number of active timeouts you may set. max active timeouts: 10000, current active timeouts: 10000, finished timeouts: 0
 ❯ Timers.setTimeout src/workers/shared/timers.worker.ts:30:11
 ❯ #enqueue src/workers/queues/broker.worker.ts:444:16
 ❯ QueueBrokerObject.batch src/workers/queues/broker.worker.ts:562:8

 Test Files  1 failed | 3 passed (4)
      Tests  1 failed | 23 passed | 1 skipped (25)

and on this branch:

 ✓ test/plugins/queues/index.spec.ts (14 tests | 1 skipped) 2594ms
   ✓ enqueues more undelayed messages than workerd's active timeout limit 395ms
 ✓ test/plugins/queues/cross-process.spec.ts (4 tests) 1003ms
 ✓ test/plugins/queues/delay.spec.ts (5 tests) 904ms
 ✓ test/plugins/queues/retry.spec.ts (2 tests) 385ms

 Test Files  4 passed (4)
      Tests  24 passed | 1 skipped (25)

File and line references are against main @ 9515011dc5ecdc5abf3a0c685d80f78e307fb513.


  • Tests
    • Tests included/updated
    • Automated tests not possible - manual testing has been completed as follows:
    • Additional testing not necessary because:
  • Public documentation
    • Cloudflare docs PR(s):
    • Documentation not necessary because: this restores the documented behaviour of send()/sendBatch() in local dev; no documented behaviour or configuration changes.

A picture of a cute animal (not mandatory, but encouraged)


Devin Review

The local Queues broker registered a timer for every message it
received, including messages with no delivery delay. workerd caps a
Durable Object at 10000 active timeouts and none of those timers run
while the producer is still sending, so enqueuing more than 10000
messages in one burst failed with QuotaExceededError.

Messages without a delivery delay are now enqueued directly and only
delayed messages use a timer. Miniflare already behaved this way under
fake timers, where Timers.setTimeout routes a zero delay through
queueMicrotask and runs the closure synchronously - which is why the
existing queues tests, all of which enable fake timers, never caught
this.

Fixes cloudflare#15670
@changeset-bot

changeset-bot Bot commented Sep 16, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 45185f0

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 8 packages
Name Type
miniflare Patch
@cloudflare/deploy-helpers Patch
@cloudflare/pages-shared Patch
@cloudflare/remote-bindings Patch
@cloudflare/runtime-types Patch
@cloudflare/vite-plugin Patch
@cloudflare/vitest-plugin Patch
wrangler Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-project-automation github-project-automation Bot moved this to Untriaged in workers-sdk Sep 16, 2026
@workers-devprod
workers-devprod requested review from a team and cjol and removed request for a team September 16, 2026 13:29
@workers-devprod

Copy link
Copy Markdown
Contributor

Codeowners approval required for this PR:

  • @cloudflare/wrangler
Show detailed file reviewers
  • .changeset/eager-brokers-enqueue.md: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/queues/broker.worker.ts: [@cloudflare/wrangler]
  • packages/miniflare/test/plugins/queues/index.spec.ts: [@cloudflare/wrangler]

@devin-ai-integration devin-ai-integration Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

Newer findings are available below. Devin Review posted a newer report on this PR, in addition to the findings presented here.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no bugs or issues to report.

Devin Review

@pkg-pr-new

pkg-pr-new Bot commented Sep 16, 2026 •

Copy link
Copy Markdown
@cloudflare/autoconfig

npm i https://pkg.pr.new/@cloudflare/autoconfig@15676

@cloudflare/build-output-utils

npm i https://pkg.pr.new/@cloudflare/build-output-utils@15676

@cloudflare/codemods

npm i https://pkg.pr.new/@cloudflare/codemods@15676

@cloudflare/config

npm i https://pkg.pr.new/@cloudflare/config@15676

@cloudflare/containers-shared

npm i https://pkg.pr.new/@cloudflare/containers-shared@15676

create-cloudflare

npm i https://pkg.pr.new/create-cloudflare@15676

@cloudflare/deploy-helpers

npm i https://pkg.pr.new/@cloudflare/deploy-helpers@15676

@cloudflare/kv-asset-handler

npm i https://pkg.pr.new/@cloudflare/kv-asset-handler@15676

miniflare

npm i https://pkg.pr.new/miniflare@15676

@cloudflare/pages-functions

npm i https://pkg.pr.new/@cloudflare/pages-functions@15676

@cloudflare/pages-shared

npm i https://pkg.pr.new/@cloudflare/pages-shared@15676

@cloudflare/runtime-types

npm i https://pkg.pr.new/@cloudflare/runtime-types@15676

@cloudflare/unenv-preset

npm i https://pkg.pr.new/@cloudflare/unenv-preset@15676

@cloudflare/vite-plugin

npm i https://pkg.pr.new/@cloudflare/vite-plugin@15676

@cloudflare/vitest-plugin

npm i https://pkg.pr.new/@cloudflare/vitest-plugin@15676

@cloudflare/workers-auth

npm i https://pkg.pr.new/@cloudflare/workers-auth@15676

@cloudflare/workers-editor-shared

npm i https://pkg.pr.new/@cloudflare/workers-editor-shared@15676

@cloudflare/workers-utils

npm i https://pkg.pr.new/@cloudflare/workers-utils@15676

wrangler

npm i https://pkg.pr.new/wrangler@15676

commit: 45185f0

@L4XB

L4XB commented Sep 17, 2026

Copy link
Copy Markdown
Author

The three red C3 E2E ... - cli jobs are an npm propagation race, not something in this PR.

rolldown@1.2.9 was published at 2026-09-16T13:28:26Z; this run started at 13:28:52Z, 26 seconds later. vite@8.3.0 depends on rolldown: ~1.2.6, so the scaffolded project resolved the brand-new version, and the C3 mock registry's npmjs uplink could not fetch its tarball yet:

ERR_PNPM_FETCH_404  GET http://localhost:41409/rolldown/-/rolldown-1.2.9.tgz: Not Found - 404
This error happened while installing the dependencies of vite@8.3.0

The same line shows up in the - cli jobs of two unrelated PRs whose runs started in the same window (#... joaquim/do-hibernation-timeout job 104813077650 and chore/issue-14301-migrate-blake3-wasm-to-noble job 104813120031), including the npm leg. C3 E2E runs from 13:41Z onward are green again, and rolldown-1.2.9.tgz now returns 200 from registry.npmjs.org.

Check and Run Codeowners Plus are already green — their first attempts were cancelled by their own concurrency groups and the re-runs passed.

Could someone with Actions write access re-run the three - cli jobs in run 35102172824? I have no way to trigger a re-run on a fork PR, and there is no code change to make for this.

For reference, locally on this branch: validate-changesets.ts exits 0, oxfmt --check and oxlint --deny-warnings --type-aware are clean on the changed files, tsc and the worker-side types build pass, and packages/miniflare queue tests are 13 passed | 1 skipped after a turbo build --filter=miniflare. The new regression test does fail (Queue sendBatch failed) when the broker change is reverted and rebuilt.

@workers-devprod

Copy link
Copy Markdown
Contributor

Codeowners approval required for this PR:

  • ✅ @cloudflare/wrangler
Show detailed file reviewers

@workers-devprod workers-devprod left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codeowners reviews satisfied

@github-project-automation github-project-automation Bot moved this from Untriaged to Approved in workers-sdk Sep 22, 2026
Comment thread packages/miniflare/test/plugins/queues/index.spec.ts Outdated
Comment thread packages/miniflare/src/workers/queues/broker.worker.ts Outdated
Co-authored-by: Christopher Little-Savage <clittle-savage@cloudflare.com>
@cjol
cjol enabled auto-merge (squash) September 22, 2026 14:42
devin-ai-integration[bot]

This comment was marked as resolved.

auto-merge was automatically disabled September 23, 2026 19:06

Head branch was pushed to by a user without write access

cloudflare#15713 removed `type: "worker"` from the worker config type, so the test added here stopped type-checking once main was merged in, and the miniflare build failed.
@L4XB

L4XB commented Sep 24, 2026

Copy link
Copy Markdown
Author

The red runs were one type error. After the main merge, the queue regression test still passed type: "worker", which #15713 removed, so the miniflare build failed and took the E2E jobs with it. 45185f0 drops the field, and the test passes locally.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Approved

Development

Successfully merging this pull request may close these issues.

[Queue] Posting many messages to a queue locally causes an exception

3 participants