We'd already written the post about this bug class. Check-then-act quota races — count something, compare to a limit, write if there's room, and let a concurrent request slip through the gap between the read and the write. We'd fixed it once in Flatline's check-creation endpoint and once in its registration endpoint. We knew the shape. We knew the fix. And it was still sitting, unpatched, in SnapOG's actual paid-tier metering endpoint — the one route where getting this wrong costs real money, not hypothetical money.
The reason it survived two prior fixes and four prior blog posts about the same bug family is worth dwelling on: this instance didn't look like the others. There was no SELECT COUNT(*) next to an INSERT. There was an in-memory field on an object, and the write that invalidated it happened after real work — an image render and a cache write — not immediately after the read. Grepping for "count then insert" wouldn't have found it. We only found it by going back to first principles: read every D1 write site in both codebases and ask, for each one, "what does this write assume is still true, and what could run between the read and this write?"
The bug
SnapOG's /og endpoint enforces a monthly image quota per API key — 100 images/month on the free tier. The check looked like this:
let apiKey = await resolveApiKey(c.env.DB, rawKey); // SELECT
apiKey = await maybeResetUsage(c.env.DB, apiKey);
if (apiKey.usage_count >= apiKey.monthly_limit) { // in-memory check
return c.json({ error: 'Monthly image limit reached' }, 429);
}
// ... R2 cache lookup, or a full image render via workers-og ...
c.executionCtx.waitUntil(
recordUsage(c.env.DB, apiKey, params.template, cacheHit) // UPDATE usage_count = usage_count + 1
);
The limit check runs against a snapshot read at the top of the request. The counter increment is a separate, unconditional UPDATE that runs — deliberately, as a fire-and-forget waitUntil — after the response's image bytes are already generated or fetched from cache. That gap isn't two adjacent database statements anymore. It's an entire request lifecycle: a database read, then (on a cache miss) a Satori-based PNG render and an R2 write, then finally the increment.
Fire enough concurrent requests at a key sitting at 99/100 and every one of them reads usage_count = 99 before any of them finishes incrementing it. Every one passes the check. Every one renders or serves an image. Every one increments afterward. A free-tier key with a stated 100-image cap can be walked arbitrarily far past it — we tested with 10 concurrent requests at the boundary and got 10 successes, landing the counter at 109 instead of 100. There's no cap on how far over; it scales with however many requests you fire in the window, and the window is wider than a bare database round-trip because real compute and R2 I/O sit inside it.
Why the wider gap matters more, not less
The Flatline bugs we'd already fixed had a race window measured in however long two sequential SQL statements take — real, exploitable, but narrow. This one has a window that includes an image-rendering library initializing a WASM runtime and doing layout, plus an R2 put. That's milliseconds to tens of milliseconds of genuine work, not microseconds of network round-trip. It's a wider, easier target, on the one endpoint in either product that's directly tied to a billing tier. Free-tier abuse here isn't just an accounting nuisance — it's unbounded R2 storage and egress and Workers CPU time, consumed against a promised cap of zero dollars.
The fix: reserve the unit before you do any work
Same fix shape as before — fold the limit check into the write itself, so the database enforces it atomically instead of application code reasoning about a stale read — but moved to run before the image work, not after it:
async function tryConsumeUsage(db: D1Database, key: ApiKey): Promise<boolean> {
const result = await db
.prepare(
'UPDATE api_keys SET usage_count = usage_count + 1 WHERE id = ? AND usage_count < monthly_limit'
)
.bind(key.id)
.run();
return (result.meta.changes ?? 0) > 0;
}
// In the route, before any R2 lookup or image render:
const withinLimit = await tryConsumeUsage(c.env.DB, apiKey);
if (!withinLimit) {
return c.json({ error: 'Monthly image limit reached', ... }, 429);
}
Two things had to change together, not just one. Folding the limit into the WHERE clause closes the race for concurrent requests hitting the database at the same instant. But that alone doesn't help if the atomic write still runs after the expensive work — you'd have a correct counter and still have already paid for and served the over-limit images. The increment has to happen first, gating the work that follows it, not recording it afterward. Usage-event logging (which template, cache hit or miss) stays a separate fire-and-forget insert — it's for analytics, not enforcement, so it doesn't need to be in the critical path.
The regression test
Same shape as the Flatline test: fire concurrent requests at the boundary, assert on the aggregate, not any single response.
const CONCURRENCY = 10;
const responses = await Promise.all(
Array.from({ length: CONCURRENCY }, (_, i) =>
request(`/og?title=Race+${i}&key=${rawKey}`)
)
);
const allowed = responses.filter(r => r.status === 200);
const limited = responses.filter(r => r.status === 429);
expect(allowed.length).toBe(1);
expect(limited.length).toBe(CONCURRENCY - 1);
const after = await env.DB.prepare('SELECT usage_count FROM api_keys WHERE key_hash = ?')
.bind(hash).first();
expect(after.usage_count).toBe(100); // exactly the limit, never over
Run this against the pre-fix code — even without a full request-lifecycle window, just the database check-then-write gap — and it's flaky-to-failing: multiple 200s land, and the final count overshoots 100. Against the fix, it's deterministic every run: exactly one 200, nine 429s, counter lands at exactly 100.
The actual lesson isn't "fold checks into writes"
We already knew that lesson; we'd shipped it twice. The lesson this instance adds is that pattern-matching on the shape of a known bug ("count next to insert") is not the same as understanding the bug class ("a read that a later write depends on, with any kind of gap in between — network, compute, I/O, or just two SQL statements — is a race"). The fix for this bug had existed in the codebase for weeks, proven and tested, one directory over, and it still didn't get applied here — because this instance didn't pattern-match on sight. The thing that actually found it was going back to the general definition and re-auditing every write site against it, not scanning for a specific code shape. If your product enforces more than one quota, the question worth asking about each one isn't "does this look like the bug I already fixed" — it's "what does this write assume is still true, and does anything — including real work, not just another database call — run between the read and the write."
Want to see the full fix and its concurrent-request test in context?