The problem
Bots try thousands of passwords on websites' sign-in forms. The interviewer asks for a CAPTCHA service that many websites use: a puzzle a person can solve and a script can't. Version 1: ask for a puzzle only after warning signs, issue it with a one-time token, check the answer once on your servers, and give each website a verify call it makes with its secret key. Park dashboards, invisible risk scores and billing.
The numbers
Say about 50 million challenges a day, up to about 2,500 a second at busy hours (50 million ÷ 100,000 seconds ≈ 500 a second on average, 5× at busy hours). Each must be answered within 2 minutes, and a record takes about 100 bytes.
what estimate
alive at once 2,500 a second × 120 s = 300,000
memory 300,000 × 100 bytes ≈ 30 MB
a day, kept about 50 million records
Alive at once is the rate times how long each one lives, in the same unit: 2,500 × 2 minutes would be off 60 times. 30 MB fits easily in one in-memory store; the hard parts are the rules below, not the size.
Decision 1: when to show a CAPTCHA
A puzzle at every sign-in annoys real people, and a bot that pays people to solve puzzles gets through anyway. Blocking an address after a few failures locks out everyone behind it (an office, a school, a phone network). Ask only after warning signs: the website counts failed sign-ins per IP and asks for a CAPTCHA after 5 in 10 minutes.
The count works like the Rate Limiter's fixed window: one counter per IP in a shared in-memory store, raised in one step, set to expire 10 minutes after the first failure. A person who mistypes once never sees a puzzle; a script reaches 5 within seconds. Count per account too, for attacks spread over many addresses.
Decision 2: what the browser gets
When the page needs a puzzle, it asks the service for a challenge. The browser gets only what the person needs, the picture and a long random token:
POST /challenges {: "pk_shop"}
→ 201 Created {: "c_8f2kQx…", : "…"}
Everything else stays in the service's store, keyed by the token:
{: "c_8f2kQx…", : "k7mQ2", : "shop", : false, : 120}
The answer never goes to the page, because a bot reads pages. The site is kept so a token made for one website can't be spent on another, and the expiry and the used flag are the service's to decide.
Decision 3: expire the unanswered ones
Bots fetch challenges by the thousand and answer few. Give each record a time to live (TTL) of 2 minutes and let the store delete it on its own: only the last 2 minutes stay, about 300,000 at busy hours, and a bot can't stockpile puzzles for paid solvers to answer hours later. A nightly cleanup job would let a day of records pile up, about 50 million; refusing late answers without deleting would keep them forever. Verify still checks the expiry time, since some stores delete expired records late.
Decision 4: the website verifies, server to server
Ana's browser sends the sign-in form to the website with the token and her answer. The website's server then asks the CAPTCHA service, with the secret key only its servers know:
POST /verify {: "sk_shop_…", : "c_8f2kQx…", : "k7mQ2"}
→ 200 OK {: true}
Never trust the browser's "passed": a bot can post passed: true without any page, and a secret in the page's code is a secret anyone can copy. Call verify before the slow password hash, so bots can't make the site hash their guesses. If verify doesn't answer, refuse and ask again: a check that guards sign-ins fails closed, as the Rate Limiter put it.
Decision 5: each token works once
A bot pays a person to solve one puzzle, then sends the same token and answer with 1,000 sign-in tries, about 8 a second for 2 minutes. If a token worked until it expired, one paid answer would buy 1,000 password guesses. Reading the token, then writing "used" has the Rate Limiter's race: two checks at the same moment both read "unused", and both pass.
set used = true, only where used = false (one step in the store)
first check changed 1 record check the answer, reply
second check changed 0 records success: false, already used
A conditional update marks the token used only if it's still unused, and the store runs such updates one at a time, so exactly one check wins. Mark it used even when the answer is wrong, or a bot could try answer after answer on one picture.
Same pattern as the Rate Limiter
Both keep small, short-lived records in a shared in-memory store that every server reads, and both depend on a check and a change in one step. The failure counter per IP is the Rate Limiter's fixed window, and the conditional update is its fix for two servers taking the last token. What's new: one-time tokens, an answer that never leaves your servers, expiry by TTL, and a verify call between servers, with a secret.
Summary
- Scope: ask after warning signs, issue a puzzle with a one-time token, check once, let sites verify.
- Numbers: about 2,500 challenges a second at busy hours, about 300,000 alive at once, about 30 MB.
- When to show: after 5 failed sign-ins from one IP in 10 minutes, counted in a shared store.
- Data: the picture and the token go to the browser; the answer, site, expiry and used flag stay on the service.
- Expiry: a 2-minute TTL on each record, and verify checks the time.
- Verify: the website's server calls POST /verify with its secret key, before the password check.
- One use: a conditional update marks each token used, right answer or wrong.