{
"$type": "site.standard.document",
"bskyPostRef": {
"cid": "bafyreihsusg4aj4juntocbw6lsolyruwjuhgmp5vnf5wlscdhduwacojte",
"uri": "at://did:plc:25rdn5elo5izoxrmtis34zuk/app.bsky.feed.post/3motr3d6nazp2"
},
"coverImage": {
"$type": "blob",
"ref": {
"$link": "bafkreibceqlwgum37bnzmdj4h4f67jmzjssrk4i3thkepgcfie6fcxu4qq"
},
"mimeType": "image/webp",
"size": 74268
},
"path": "/riversea/400-ad-accounts-x-3-subrequests-silent-data-loss-in-cloudflare-workers-and-how-i-fixed-it-43ie",
"publishedAt": "2026-06-22T01:07:59.000Z",
"site": "https://dev.to",
"tags": [
"cloudflare",
"serverless",
"webdev",
"tutorial",
"Full post →"
],
"textContent": "Adding a single KV cache write broke my Worker — and it didn't throw a single error to the caller.\n\nI run a Korean D2C ad-ops pipeline where one Worker fans out performance checks across ~400 ad accounts per request. The math looked fine: 400 accounts × 2 API calls = 800 subrequests, safely under Cloudflare's hard 1000-per-invocation ceiling. Then I added KV writes to cache each result. 400 × 3 = 1200. The Worker silently stopped processing the last ~200 accounts. No 5xx. No exception propagated. Just missing data. It took two days on `wrangler tail` to surface this line:\n\n\n\n workers/runtime/fetch: subrequest count exceeded limit of 1000\n\n\nThe thing that makes this limit especially tricky for fan-out patterns: it resets _per invocation_ , not per batch or queue message. And the count includes more than you'd expect — every `fetch()`, every KV `get`/`put`, every R2 operation, every D1 query, every Durable Object stub call. The one useful exception: `Queue.sendBatch()` does **not** count against it. That's the escape hatch the fix depends on.\n\nThe architectural answer is splitting the fan-out trigger from the fan-out work. The entry Worker receives the request and immediately calls `env.QUEUE.sendBatch()` with one message per account — zero upstream fetches, zero KV writes, well under any limit. Each queue consumer invocation then handles a small batch of messages (I run `max_batch_size = 10`), doing its 3 subrequests per account in isolation. Ten messages × 3 subrequests = 30 per invocation. The ceiling becomes irrelevant.\n\nThe tradeoff worth knowing before you commit to this pattern: Queues are fire-and-forget. If your use case requires returning all 400 account summaries in a single synchronous HTTP response — like a live dashboard — this approach alone won't get you there. That's a different problem requiring a Durable Object as an aggregation point, which adds its own subrequest considerations.\n\nI wrote up the full breakdown — including the Durable Object aggregation pattern for synchronous fan-out, the exact `wrangler.toml` consumer config, and the complete subrequest reference table — over on dailymanuallab.com.\n\nFull post →",
"title": "400 ad accounts 3 subrequests = silent data loss in Cloudflare Workers (and how I fixed it)"
}