Skip to content

Make the free backlink checker's daily spend cap atomic - #229

Open
harshinsecurity wants to merge 1 commit into
every-app:mainfrom
harshinsecurity:fix/backlink-check-atomic-budget
Open

Make the free backlink checker's daily spend cap atomic#229
harshinsecurity wants to merge 1 commit into
every-app:mainfrom
harshinsecurity:fix/backlink-check-atomic-budget

Conversation

@harshinsecurity

Copy link
Copy Markdown

TL;DR

DAILY_CHECK_BUDGET is meant to cap the free backlink checker at 500 paid
DataForSEO 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.ts reads 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_BUDGET to 5 and issuing 40
simultaneous checks for distinct domains:

reached the paid call
before 33 of 40
after 5 of 40

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.

  • main moves to a new web/src/server.ts, because Durable Object classes must
    be named exports of the Worker entry. This mirrors the app's existing
    src/server.ts.
  • Preview keeps sharing production's budget via script_name, matching the
    intent of the shared KV namespace it replaces.
  • BACKLINK_CHECK_KV is now unused and removed from the config. The namespace
    itself can be deleted separately.

types:check, prettier --check, and a full vite build pass, and
prerendering 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_name cross-worker binding
for preview is the one piece only a real deploy will confirm.

I know from docs/CONTRIBUTING.md that external PRs aren't being merged right
now. 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

  1. wrangler secret list --name open-seo-landing. I couldn't find any Turnstile
    code in the deployed bundles reachable from main.js. If
    TURNSTILE_SECRET_KEY is set while the deployed build lacks
    VITE_TURNSTILE_SITE_KEY, checks would currently be returning 403 to real
    users — which is the case the error string at that branch already anticipates.
  2. Capping the DataForSEO account (prepaid balance, auto-recharge off) bounds
    worst-case spend regardless of what the code does.
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

1 participant