CYART // SCHEMA · EDGE-DDOS7 NODES · 7 LINKSFLOODDROP 99%MISS ONLYTHREAT · Botnet — 100 Gbps+, L3/L4THREAT01Botnet100 Gbps+, L3/L4ACTOR · Real usersACTOR02Real usersEDGE · Anycast — same IP, 100+ PoPsEDGE03Anycastsame IP, 100+ PoPsEDGE · L4 scrubbing — SYN cookies, rate capsEDGE04L4 scrubbingSYN cookies, rate capsEDGE · L7 WAF — rules, bot score, challengeEDGE05L7 WAFrules, bot score, challengeDATA · Cache — absorbs readsDATA06Cacheabsorbs readsSERVICE · Origin — allow-list PoP IPs onlySERVICE07Originallow-list PoP IPs only

Design a bot detection system

Design the system that tells humans from automation on a busy consumer site: TLS and HTTP/2 fingerprints like JA4, device and behavioural signals, a cheap score at the edge and a heavier one offline, a challenge ladder instead of a block button, and the feedback loop that keeps it working once attackers adapt.

● MediumFREEDesign problem·21 min read·Intermediate → Advanced
bot detectionJA4TLS fingerprintingbot managementCAPTCHAcredential stuffing

01Understand the problem and size it

✓What you will learn
  • Name the signal families: network (IP, ASN), protocol (JA4, HTTP/2 fingerprint), device (JS-collected traits) and behaviour
  • Split scoring into a sub-millisecond edge score and an asynchronous aggregate score that feeds it
  • Design a challenge ladder (silent, proof-of-work, interactive, block) instead of a binary allow/deny
  • Handle privacy, evasion (curl-impersonate, residential proxies, CAPTCHA farms) and label feedback
◎Interview radar

What they probe: which signals survive an attacker who controls every header, where the score is computed and how fast, what happens on a false positive, how the system learns once attackers adapt. Where candidates slip: user-agent and IP rules only, a CAPTCHA on every suspicious request, returning the reason for a block to the client, no plan for labels.

A bot here is any client driven by software rather than a person: scrapers, credential stuffers, scalpers buying limited stock, fake sign-up farms. Some bots are wanted (search crawlers, uptime monitors, partner APIs), so the system must also verify good bots, usually by reverse-then-forward DNS of the crawler IP or a published IP list.

Functional requirements

  1. Score every HTTP request to protected routes (login, sign-up, checkout, search) for likelihood of automation.
  2. Act on the score per route: allow, silently challenge, interactively challenge, rate limit, or block.
  3. Let verified good bots through and let product teams set per-route policy without a deploy.
  4. Feed outcomes (challenge results, fraud labels, support reports) back into rules and models.

Non-functional requirements

  1. 300k requests/s peak across 200 edge points of presence (PoPs).
  2. Edge decision adds < 1 ms p99; the site must not slow down for humans.
  3. False positive rate on humans < 0.1 % on checkout, because a blocked buyer is lost revenue.
  4. Collect the minimum personal data, with documented retention.

Back of the envelope

Sizingtxt
peak requests              300,000 / s across 200 PoPs  = 1,500 / s per PoP on average
edge scoring CPU           0.5 ms per request
  -> 300,000 x 0.5 ms      = 150 core-seconds per second = 150 cores fleet-wide
request metadata to async  400 B per request (JA4, H2 fp, IP, ASN, route, status, cookie id)
  -> 300,000 x 400 B       = 120 MB/s ≈ 10.4 TB/day raw
aggregate state            50 M distinct IPs/day x 100 B ≈ 5 GB  (fits in memory per region)
model push to edge         every 5 min, a few MB of lists + weights
▭On the whiteboard · Scope3 points
01
FR
score every protected request | per-route actions | good bots verified | outcomes fed back
02
NFR
300k rps | < 1 ms p99 at edge | < 0.1 % human FP on checkout | minimal personal data
03
out
WAF rule authoring, DDoS volumetric scrubbing (separate edge layer)

02High-level design and the signals

FigWhere bot scoring sits
CYART // SCHEMA · EDGE-DDOS7 NODES · 7 LINKSFLOODDROP 99%MISS ONLYTHREAT · Botnet — 100 Gbps+, L3/L4THREAT01Botnet100 Gbps+, L3/L4ACTOR · Real usersACTOR02Real usersEDGE · Anycast — same IP, 100+ PoPsEDGE03Anycastsame IP, 100+ PoPsEDGE · L4 scrubbing — SYN cookies, rate capsEDGE04L4 scrubbingSYN cookies, rate capsEDGE · L7 WAF — rules, bot score, challengeEDGE05L7 WAFrules, bot score, challengeDATA · Cache — absorbs readsDATA06Cacheabsorbs readsSERVICE · Origin — allow-list PoP IPs onlySERVICE07Originallow-list PoP IPs only
  • ACTOR
  • EDGE
  • SERVICE
  • DATA
  • THREAT
  • Live flow
//Requests hit anycast edge PoPs. The edge computes protocol fingerprints during the TLS and HTTP/2 handshake, joins cached reputation and model weights, scores, and applies the route policy. Request metadata streams to a regional analytics tier that aggregates by fingerprint, IP and session and pushes updated lists and weights back to the edge.

Signal families

FamilyExamplesCost to fakeNotes
NetworkIP reputation, ASN type (hosting vs residential vs mobile), Tor exit, geoLow: residential proxies sell clean IPs by the GBGood for priors, weak alone; mobile carrier NAT puts thousands of humans behind one IP
TLS fingerprintJA4 of the ClientHello: TLS version, cipher and extension counts, ALPN, hashes of sorted ciphers and extensionsMedium: needs a TLS stack that mimics a browser (curl-impersonate, utls)Cannot be set by a header; mismatch with the claimed User-Agent is a strong signal
HTTP/2 fingerprintSETTINGS values and order, WINDOW_UPDATE size, pseudo-header orderMediumPython and Go HTTP/2 clients differ visibly from Chrome
Device (JS)navigator.webdriver, canvas and WebGL traits, fonts, screen, timezone vs IP geoMedium to highOnly on pages that run your script; headless stealth plugins patch the obvious ones
Behaviourpointer paths, keystroke timing, time-to-submit, navigation orderHigh to fake at scaleNeeds a session of activity; privacy-sensitive
Historyper-fingerprint and per-IP rates, challenge pass rate, account outcomesHighOnly the async tier can compute it

JA4 in one paragraph

JA3 (2017) hashed the raw ClientHello fields with MD5. When Chrome began randomising TLS extension order in early 2023, every Chrome connection got a different JA3. JA4 (FoxIO, 2023) fixes that by sorting ciphers and extensions before hashing and by putting readable fields up front. A JA4 has three parts separated by underscores: a readable prefix, a truncated SHA-256 of the sorted cipher list, and a truncated SHA-256 of the sorted extensions plus signature algorithms.

Reading a JA4 and spotting a mismatchtxt
JA4 = t13d1516h2_8daaf6152771_e5627efa2ab1
       | | | | | |  |            |
       | | | | | |  |            +-- sha256(sorted extensions + sig algs)[:12]
       | | | | | |  +--------------- sha256(sorted cipher suites)[:12]
       | | | | | +------------------ ALPN first and last char: "h2"
       | | | | +-------------------- 16 extensions
       | | | +---------------------- 15 cipher suites
       | | +------------------------ d = SNI is a domain (i = IP, no SNI)
       | +-------------------------- TLS 1.3
       +---------------------------- t = TCP (q = QUIC)

request:  User-Agent: Chrome/131 on Windows
          JA4 seen:   t13d1312h2_...   (a Go net/http stack, not Chrome)
verdict:  claimed browser and TLS stack disagree -> strong automation signal

Two scoring tiers

  • Edge (synchronous): features available from this request alone plus pushed state: JA4 and HTTP/2 fingerprint, UA consistency, IP and ASN reputation, cookie validity, a small gradient-boosted model or weighted rules. Budget 0.5 ms CPU, no network calls.
  • Async (regional stream): aggregates over windows: requests per JA4 per ASN per minute, login failures per fingerprint, distinct accounts per device cookie, challenge pass rates. Produces lists (fingerprint plus ASN pairs to challenge, IP ranges to rate limit) and refreshed weights pushed to the edge every few minutes.
  • Session cookie: once a client passes a challenge, the edge issues a signed, short-lived cookie (HMAC over cookie ID, JA4, IP prefix, expiry) so it is not challenged on every request. Binding to JA4 stops a solved cookie being replayed from a different stack.
▭On the whiteboard · Happy path3 points
01
edge
fingerprint at handshake → local score → route policy → action
02
async
stream metadata → windowed aggregates → lists + weights pushed every 5 min
03
memory
signed clearance cookie bound to JA4 + IP prefix, 30 min

03Deep dives: challenges, privacy, evasion

The challenge ladder

A block teaches the attacker exactly what tripped. A ladder costs bots more than humans and gives you labels at each rung.

Score bandActionHuman costBot cost
0 to 30AllowNoneNone
30 to 60Silent JS check plus small proof-of-work (about 100 ms on a phone)InvisibleMust run a real JS engine; PoW adds CPU per request at scale
60 to 85Interactive challenge or Private Access TokenA few secondsCAPTCHA farm fees, about $1 to $3 per 1,000 solves
85 to 100Block, tarpit, or serve plausible decoy dataHigh if wrongWasted requests, poisoned scraped data

Thresholds are per route: search can tolerate more automation than checkout. Private Access Tokens (the Privacy Pass protocol, RFC 9576 to 9578) let a device attest "a real device solved a check" through an issuer without revealing identity, which removes the CAPTCHA for most Apple users.

Edge scoring logic

Edge score: pure, bounded, no network callsts
type Action = "allow" | "silent" | "interactive" | "block";

interface EdgeFeatures {
  ja4: string;
  uaFamily: "chrome" | "firefox" | "safari" | "other";
  asnType: "residential" | "mobile" | "hosting" | "unknown";
  ipRep: number;            // 0 (clean) .. 1 (known bad), from pushed list
  hasValidClearance: boolean;
}

const JA4_RE = /^[tqd][0-9a-z]{2}[di][0-9]{4}[0-9a-z]{2}_[0-9a-f]{12}_[0-9a-f]{12}$/;

export function score(f: EdgeFeatures, knownBrowserJa4: Map<string, string>, listed: Set<string>): number {
  if (!JA4_RE.test(f.ja4)) return 90;                       // malformed or missing: treat as hostile
  if (f.hasValidClearance) return 10;
  let s = 20 * f.ipRep;
  if (f.asnType === "hosting") s += 25;
  const family = knownBrowserJa4.get(f.ja4);                 // ja4 -> browser family it belongs to
  if (family === undefined) s += 15;                          // unseen stack: new browser build or a tool
  else if (f.uaFamily !== "other" && family !== f.uaFamily) s += 40; // claims a browser, TLS says otherwise
  if (listed.has(f.ja4 + "|" + f.asnType)) s += 30;          // async tier flagged this pair
  return Math.min(100, Math.max(0, Math.round(s)));
}

export function decide(s: number, route: { silent: number; interactive: number; block: number }): Action {
  if (s >= route.block) return "block";
  if (s >= route.interactive) return "interactive";
  if (s >= route.silent) return "silent";
  return "allow";
}

Privacy

  • Behavioural and device signals are personal data under GDPR when tied to a user. Collect them for security under legitimate interest, document it, and keep raw pointer traces only as derived features, not recordings.
  • Hash IPs with a rotating keyed hash in the analytics tier; keep raw IPs at the edge for minutes, not months.
  • Retain request metadata 30 days for training and investigations, aggregates longer.

Evasion and how the design answers it

EvasionWhat it beatsAnswer
curl-impersonate, utlsJA4 and HTTP/2 mismatchDevice JS checks and behaviour; impersonation libraries lag new browser releases, so stale fingerprints become a signal
Headless Chrome with stealth patchesSimple navigator.webdriver checksConsistency checks across dozens of traits; PoW cost at volume; behaviour on forms
Residential proxy poolsIP reputation and per-IP limitsAggregate by fingerprint and device cookie rather than IP; ASN priors
CAPTCHA farmsInteractive challengesPrice the attack: rate limits per account and per payment instrument downstream; do not rely on the challenge alone
!Never explain a block to the client

Returning "blocked: TLS fingerprint mismatch" hands the attacker a free oracle. Return a generic page with a reference ID; the reason lives in your logs. For the same reason, enforce some list updates with a random delay so attackers cannot bisect which change caught them.

04Failure modes, feedback loops and trade-offs

Failure modes

FailureBehaviourWhy
Async tier downEdge keeps last pushed lists and weights; alert if older than 30 minEdge must never block on the analytics path
Bad model pushCanary to 1 % of PoPs, auto-rollback if challenge rate jumps > 2xA bad push is an outage for real buyers
Challenge provider downFall back to PoW plus tighter rate limitsNever fail open to unlimited login attempts
Mobile app trafficNo JS; use app attestation (Play Integrity, App Attest) insteadThe ladder needs a native equivalent

Labels and feedback loops

  • Weak labels arrive fast: challenge pass or fail, PoW completed, cookie reuse patterns.
  • Strong labels arrive slowly: accounts later confirmed fraudulent, chargebacks on scalped orders, users reporting takeover.
  • Selection bias: blocked traffic never shows its true outcome. Keep a small random holdout (for example 1 % of mid-band traffic is challenged instead of blocked) so you can measure what you would have caught.
  • Adversarial drift: watch the score distribution per route daily; a sudden shift towards the middle bands usually means a new evasion.
iGood bots

Verify crawlers by reverse DNS of the source IP (it must resolve to the crawler's documented domain) and then forward DNS back to the same IP. A User-Agent saying Googlebot proves nothing.

Trade-offs to state

ChoiceGainCost
Edge score vs central scoring serviceNo added latency, survives regional outageOnly per-request features; aggregates arrive minutes late
Challenge vs blockLabels, fewer lost humansAttackers can pay farms; needs a challenge vendor
Binding clearance to JA4Cookie replay across stacks failsBrowser updates change JA4 and force re-challenge
Decoy data vs blockPoisons scrapers, hides detectionMust never reach real users or search engines

05Final design, follow-ups and self-test

▭On the whiteboard · Final design3 points
01
edge
JA4 + HTTP/2 fp at handshake, local score < 0.5 ms, per-route ladder, signed clearance
02
async
windowed aggregates by fingerprint, ASN, cookie; lists + weights pushed every 5 min with canary
03
learn
challenge outcomes + fraud labels, 1 % holdout, drift alarms, no reasons leaked
“How to say it in the room

"I'd score at the edge with what the handshake gives me for free: JA4, the HTTP/2 fingerprint and whether they agree with the User-Agent, plus pushed IP and ASN reputation. That's half a millisecond and no network calls. A regional stream aggregates by fingerprint and device cookie, not just IP, because residential proxies make IPs cheap, and pushes lists back every few minutes. Actions are a ladder, silent proof-of-work, then an interactive challenge, then block, tuned per route, and every rung gives me labels. I keep a small holdout so I can measure what I'm missing, and I never tell the client why it was blocked."

Q1How do you protect a mobile app API that cannot run JavaScript challenges?+

Use platform attestation (Play Integrity on Android, App Attest on iOS) to prove a genuine app on a genuine device, bind the result to a short-lived token, and keep the same aggregate scoring by device and account.

Q2A big customer complains their legitimate integration is being challenged. What do you do?+

Give them an authenticated API path (API keys or mTLS) that bypasses the browser ladder but has its own rate limits. Do not allow-list by IP or User-Agent, which attackers copy.

Q3How would you detect a new evasion you have never seen?+

Unsupervised signals: sudden new JA4 values with high request share, clusters of sessions with identical timing, conversion-rate collapses per route. Then label a sample by hand and add features.

Q4Why not just rate limit by IP?+

Residential proxy pools spread an attack over tens of thousands of IPs at a few requests each, while mobile carrier NAT puts thousands of real users behind one IP. Per-IP limits block humans and miss bots.

Test yourself

A request claims to be Chrome 131 on Windows but its JA4 matches the Go standard library TLS client. What is the best interpretation?

Finished this one?
Tick it and it shows as done on the roadmap and the course.