Make the free backlink checker's daily spend cap atomic - #229
Open
harshinsecurity wants to merge 1 commit into
Open
Make the free backlink checker's daily spend cap atomic#229harshinsecurity wants to merge 1 commit into
harshinsecurity wants to merge 1 commit into
Conversation
The daily budget was a KV read-modify-write. Concurrent requests can all read the same count, all pass the check, and all bill DataForSEO while the stored count advances by one, so DAILY_CHECK_BUDGET bounded serialized checks rather than actual spend. Workers KV also serves reads from a colo-local cache, so even spaced-out requests can read a stale count. Reserve from a Durable Object instead: one instance globally, and its input gates serialize the read-modify-write, so N concurrent reservations consume N units. Reservation now fails closed — a spend cap that opens under error isn't a cap. Verified locally by lowering DAILY_CHECK_BUDGET to 5 and issuing 40 simultaneous checks: before, most of the burst reached the paid call; after, exactly 5 did. Preview keeps sharing production's budget via script_name, matching the intent of the shared KV namespace it replaces. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
oluwaseunmauwedo
pushed a commit
to oluwaseunmauwedo/open-seo
that referenced
this pull request
Aug 25, 2026
ramonmnavarro-byte
pushed a commit
to ramonmnavarro-byte/open-seo
that referenced
this pull request
Aug 25, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
TL;DR
DAILY_CHECK_BUDGETis meant to cap the free backlink checker at 500 paidDataForSEO lookups a day. It currently caps serialized checks rather than spend:
requests that overlap can each reach the paid call while the stored count
advances by one.
What is happening
The budget counter in
web/src/routes/api/backlink-check.tsreads from KV,compares, then writes back. Overlapping requests all read the same value, all
pass the check, and all reach the paid call. Workers KV also serves reads from a
colo-local cache, so the count a colo sees can be stale even without overlap.
Measured locally by lowering
DAILY_CHECK_BUDGETto 5 and issuing 40simultaneous checks for distinct domains:
Miniflare doesn't emulate KV's edge read-cache, so real-world overshoot should
be larger rather than smaller.
What this changes
Reserves from a Durable Object instead — one instance globally, and its input
gates serialize the read-modify-write, so N concurrent reservations consume N
units. Reservation fails closed, since a spend cap that opens under error isn't
a cap.
mainmoves to a newweb/src/server.ts, because Durable Object classes mustbe named exports of the Worker entry. This mirrors the app's existing
src/server.ts.script_name, matching theintent of the shared KV namespace it replaces.
BACKLINK_CHECK_KVis now unused and removed from the config. The namespaceitself can be deleted separately.
types:check,prettier --check, and a fullvite buildpass, andprerendering is unaffected. Normal paths are unchanged: a single check still
reaches DataForSEO, an invalid domain still returns 400.
Caveats
Verified in Miniflare, not on Cloudflare. The
script_namecross-worker bindingfor preview is the one piece only a real deploy will confirm.
I know from
docs/CONTRIBUTING.mdthat external PRs aren't being merged rightnow. Filing this as the proof-of-concept form described there — happy for it to
be closed and reimplemented however you prefer.
Two things worth checking regardless of this patch
wrangler secret list --name open-seo-landing. I couldn't find any Turnstilecode in the deployed bundles reachable from
main.js. IfTURNSTILE_SECRET_KEYis set while the deployed build lacksVITE_TURNSTILE_SITE_KEY, checks would currently be returning 403 to realusers — which is the case the error string at that branch already anticipates.
worst-case spend regardless of what the code does.