Design a distributed IP denylist and country ban enforcement
Block listed IPs and whole countries at every edge within seconds, for IPv4 and IPv6.
- Caching strategies — Read-through, write-through, write-back, TTLs and invalidation.
- Replication — Keeping copies so reads scale and a dead machine loses nothing.
- CDNs and edge caching — Copies of static bytes near the user, and how they stay fresh.
- Versioning data and APIs — Keeping old readers and old records working while things change.
- Bloom filters — A tiny set that says 'definitely not here' for almost no memory.
01The design, in the six-step framework
1. Requirements
The reported failure in this round is instructive: a candidate assumed IPv4 only and invented a load-balancer count. Ask about IPv6, where enforcement happens, and how large the lists are before drawing anything.
Functional
- Maintain denylists: individual IPs, CIDR ranges, and whole countries (via a country-to-range mapping that itself changes), for IPv4 and IPv6.
- Enforce at every entry point (edge proxies, load balancers, API gateways) within seconds of a change; a lookup API answers "is this address blocked, and why".
- Sources: security team entries, automated abuse feeds, and legal country bans, each with an expiry and an audit trail.
- Allowlist exceptions that win over blocks (partners, health checkers).
- Out of scope: deciding who to block (abuse detection), DDoS volumetric mitigation.
Non-functional
- Scale: 10,000 edge nodes worldwide, 20 million requests a second, 5 million list entries (ranges and addresses), 50,000 changes a day.
- Latency: the check adds under 50 microseconds per request at the edge; it must not involve a network call.
- Propagation: a new block is enforced everywhere within 10 seconds; a removal within 10 seconds too (unblocking a wrongly blocked customer matters as much).
- Correctness: no false allows for listed addresses; false blocks only from the list itself, never from the mechanism.
5 × 106 entries × ~40 bytes (IPv6 prefix + metadata) = 200 MB uncompressed; a compact trie or sorted range table is ~60 MB, fine on every edge node
20 × 106 req/s × 50 µs = 1,000 CPU-seconds per second across 10,000 nodes = 0.1 core per node: negligible if the lookup is in memory
Change fan-out: 50,000 changes/day ≈ 0.6/s × 10,000 nodes = 6,000 small messages/s: easy through a push tree
2. Core entities
- Entry: entry id, kind (address, range, country), value (prefix with length, or country code), action (block, allow), reason, source, created by, expires at, version.
- CountryMap: a versioned mapping from country code to the list of IPv4 and IPv6 prefixes, from a geolocation provider, refreshed weekly.
- ListVersion: a monotonically increasing version of the compiled list, with a delta from the previous version.
- Decision: address, matched entry, action, list version; logged in samples for debugging.
3. API
POST /entrieswith kind, value, action, reason, ttl;DELETE /entries/{id}; both return the list version that includes the change.GET /lookup?ip=2001:db8::1returns blocked or allowed, the matching entry and the version; for tools and for services without the embedded library.- Edge-facing:
WATCH list since versionstreaming deltas;GET /list/snapshotfor cold start. - In-process at the edge:
check(ip)→ decision, a pure memory lookup.
4. High-level design
- An operator or feed adds an entry; the API validates it (parseable prefix, sane length, expiry) and writes it with the next version.
- The compiler turns the entries plus the country map into a compact lookup structure and a delta from the previous version, both versioned.
- Regional distributors hold the latest snapshot and push deltas to edge nodes over long-lived streams; new nodes pull the snapshot once.
- Each edge node checks every connection's source address against its in-memory table before doing any other work, and emits a sampled decision log.
5. Deep dives
What is the lookup structure, and how does it handle IPv6 and overlapping ranges?
Addresses are matched by longest prefix. A binary trie over the address bits (32 for IPv4, 128 for IPv6) gives a lookup in at most 128 steps, well under 50 microseconds, and handles overlaps naturally: walk down, remember the last node with an entry, and the deepest match wins. A more compact alternative is a sorted array of (start, end, action) ranges with binary search, which is faster in cache but needs overlaps resolved at compile time. Allow entries are entries like any other; the compiler resolves conflicts so an allow for a specific address inside a blocked country produces a deeper match that wins. Say the rule plainly: specific beats general, and allow beats block at equal length. The IPv6 point is not academic: a /64 is one customer, a /32 is an ISP, and blocking "an address" in IPv6 usually means blocking a /64 (in-memory table design).
How does a change reach 10,000 nodes in 10 seconds, in order, including removals?
Push, not poll, through a two-level tree: compiler to regional distributors to edges, each hop a streaming connection carrying (from version, to version, delta). A node applies a delta only if from version matches what it holds; otherwise it fetches the snapshot. Removals are deltas too, so unblocking is as fast as blocking, which is what makes a false block survivable. Versions make replication and ordering trivial: a node never applies an older version over a newer one. A node that cannot reach its distributor keeps enforcing its last version and alarms after a minute of staleness; the console shows coverage (the fraction of nodes at the latest version) so the operator knows when a block is actually global.
How do country bans work when the country map changes?
A country ban is an entry "block country X", expanded by the compiler into that country's current prefixes from the geolocation map. When the map is refreshed (weekly, or on demand), the compiler re-expands every country entry and emits the delta, so prefixes that moved between countries are handled without operator action. Keep the expansion in the compiler, never at the edge, so edges stay simple and the number of expanded prefixes (tens of thousands per large country) is bounded and known. Legal bans often require a specific error page and a reason code; the entry carries both, and the edge returns them.
Where exactly is enforcement, and what about proxies and CDNs?
Enforce at the first hop that terminates the connection: the layer-4 balancer for raw drops (cheapest, before TLS), and again at the layer-7 proxy where the real client address may differ because of a trusted CDN or proxy in front. Only trust forwarded-address headers from known upstream ranges, which is itself an allowlist entry kind. For requests arriving through a CDN, the CDN edge runs the same embedded library, so the check happens at the true edge. A bloom filter on exact addresses in front of the trie is an optional micro-optimisation that answers "definitely not listed" in a few nanoseconds for the 99 percent of traffic that is clean.
6. What a strong answer sounds like
- Ask the IPv6 question first and let it shape the data structure; interviewers remember who asked.
- Size the list and the lookup cost, then insist on in-memory checks with no network call.
- Describe the versioned push tree with ordered deltas, equal speed for removals, and coverage reporting.
- Explain longest-prefix matching with allow exceptions, and country expansion in the compiler.
- Follow-ups to expect: rate of change in an attack (batch entries into one version per second), auditing who blocked a customer (entry history with source and reason), and testing a block before enforcing it (a shadow mode that logs matches without acting).
02Where this question was reported
- Hello Interview · First of 5 commonly asked Google L5 questions
- Hello Interview · Commonly asked L6
- Hello Interview · Commonly asked EM
- PracHub · 'Enforce a Country's IP Ban List on Incoming Connections Using Its Lookup API', Senior+
- LeetCode Discuss · Interviewer probed different design areas to assess L5 depth (search snippet)
- PracHub · Failed round: assumed IPv4 without asking about IPv6, invented a load-balancer count