skip to content

When is a shipped bloom filter the wrong answer for a client-side blocklist, and what would you ship instead?

level: principalimportance: should knowfreq 30%

answer

  1. who pays when the answer is wrong
  2. is there a cheap way to confirm a hit
  3. the filter buys constants, not growth
  4. irreversible user-visible actions need certainty
  5. someone must own sizing and rebuilds

basics

~20 s

It is wrong whenever a hit cannot be cheaply confirmed, because the positive then becomes an irreversible user-visible decision made on a probability. Ship an exact structure, a server-side check, or a curated subset instead, and reserve the filter for cases with a confirmation path.

solid answer

~50 s

Frame the decision around three constraints, not around memory. First, **is a hit confirmable?** If a positive routes to a cheap authoritative check, the filter is buying you an enormous reduction in checks; if the positive *is* the decision — blocking, locking, charging, warning a user — then some fraction of innocent inputs get that treatment permanently, and no error rate is small enough. Second, **does memory actually bind?** Bits per element are independent of key length, so a huge list of long identifiers is the sweet spot; a modest list, or one where you need the reason for each entry, is better served by an exact structure. Third, **can the team operate it?** You cannot delete or resize in place, so shipping one commits you to sizing for the final count, a rebuild-and-redistribute pipeline, and monitoring the observed rate against the design target. Also expect adversaries: with public parameters an attacker can mine inputs that trigger the fallback path, so keyed hashing per release matters.

go deeper

for a junior

Know the shape of the tradeoff: a filter is chosen when a set is too big to ship exactly and an occasional wrong 'maybe' is tolerable. Be able to say that a hit normally triggers a real check rather than a decision.

for a middle

Explain what the structure cannot provide — no payload per entry, no enumeration, no deletion — and why those absences, not memory, usually decide the question. Give one workload it fits and one it does not.

for a senior

Price the false-positive path against real traffic and defend a specific error target, then describe the rebuild pipeline, the monitoring signal, and what you would change if the element count doubled unexpectedly.

for a principal

Own the decision as a set of conditions rather than a preference: overwhelmingly negative traffic, a list that dwarfs the memory budget, a cheap confirmable positive, and a team that will maintain sizing and rebuilds. Say plainly which clause failing would flip your recommendation.

## Start from who pays for a wrong answer The memory argument is the easy part and it is not the decision. A filter converts a large exact question into a cheap probabilistic one with a one-sided error, and the whole judgment is whether the system has somewhere to put that error. Ask, before anything else: when the structure says 'possibly present', what happens next? If the answer is 'we make a call to the authoritative service, or read the file, and find out', the design works, and the filter's job is to remove the ninety-something percent of queries that never needed the check. If the answer is 'we show a warning', 'we block the request', 'we lock the account', 'we decline the transaction' — the false positives *are* user-visible harm, deterministically, for the unlucky inputs, forever, because the same input always gets the same answer until the filter is rebuilt. A one-in-ten-thousand rate sounds fine until you multiply it by a fleet's daily query volume and notice you are producing a steady stream of wrongly-blocked users with no path to appeal that a support engineer can even reproduce. ## The case where it clearly wins A client-side check against a very large blocklist of remote addresses is close to the ideal case. The list is enormous relative to what you can ship to a device; the entries are long strings, so the exact structure's footprint grows with length while the filter's does not; the traffic is overwhelmingly negative, so almost every query is answered exactly and locally; and a hit can be confirmed with a single lookup against a service before anything user-visible happens. Here you defend the tradeoff to a skeptic on those four points, in that order, and you note that shipping the exact set is not merely bigger — it may be impossible within the device's storage and update budget, and pushing every check to a server costs a round trip on *every* navigation instead of on a small fraction. ## The cases where it loses - **No confirmation path.** The positive is the decision. Ship an exact structure, or move the decision server-side, or accept a smaller curated list you can hold exactly. - **You need more than membership.** A blocklist entry usually wants a reason, a category, an expiry, a severity. The filter answers one bit about one input and can carry no payload, so if the product needs 'why', you need a real map for at least the entries that matter. - **The list is small enough already.** If an exact compact structure fits the budget, take exactness for free. The filter earns its keep only when memory genuinely binds. - **Frequent, fine-grained removals.** No deletion in the standard form, and a counting variant multiplies memory by several times. If entries churn hourly and you cannot rebuild and redistribute on that cadence, choose differently. - **Adversarial input with public parameters.** If the array and hash functions ship to clients, an attacker can search for inputs that light up all the required positions and use them to drive traffic into the confirmation path or the block path at will. Per-release keyed hashing raises the cost of that; it does not remove the class of problem, and a system where the fallback path is the fragile one should not be gated this way. ## What growth does and does not fix A skeptic will ask what happens at ten times the list size. Be honest: at a fixed error rate the memory is *linear* in the element count, so ten times the list is ten times the bits. The filter changes the constant — often by an order of magnitude against an exact set — but it does not change the growth. The levers when you hit the ceiling are: loosen the error rate, which costs only a few bits per element per order of magnitude and so gives you very little room; ship only the highest-value subset locally and let the tail go to the server; partition the key space and ship the partitions a client actually needs; or move the check server-side entirely and pay the latency. Naming those in order, with the memory ceiling per device as the binding constraint, is the answer an interviewer is looking for at this level. ## The operational commitment Shipping one is a commitment, not a data-structure choice. It obliges the team to size for the final element count rather than today's, since overfilling silently degrades the rate; to record the parameters alongside the artefact so a future engineer can rebuild it; to run a rebuild-and-redistribute pipeline because entries cannot be removed in place; and to measure the observed false-positive rate — every confirmed-negative hit is a sample — against the design target, alarming on drift. If nobody will own that, the clever compact artefact becomes a mystery blob that slowly gets worse, and the right recommendation is the boring exact structure that any engineer can reason about. ## How to close the argument The defensible position is not 'filters are great' or 'filters are risky'. It is: this workload is overwhelmingly negative, the list dwarfs the memory budget, a hit has a cheap confirmation, and we will own the sizing and rebuild discipline — therefore the filter. Change any one of those four clauses and the recommendation changes with it.

  • The blocklist is expected to grow tenfold — does the filter still save you?
    It saves the same constant factor, not the growth: at a fixed error rate the memory is linear in the element count, so ten times the entries is ten times the bits. The levers are loosening the rate, which buys little, shipping only a high-value subset, partitioning by key space, or moving the check server-side.
  • How could an attacker abuse a client-side filter whose parameters are public?
    By searching for inputs whose positions are all already set and using them to force traffic into whatever the positive path is — the confirmation service, or the block action itself. Per-release keyed hashing makes that search deployment-specific and expensive, but a system whose fallback path is the fragile one should not be gated by a probabilistic check.
  • What operational discipline does shipping a filter commit the team to?
    Sizing for the final element count rather than today's, recording the parameters with the artefact so it can be rebuilt, running a rebuild-and-redistribute pipeline because entries cannot be removed in place, and monitoring the observed rate — every confirmed-negative hit is a sample — against the design target.
  • A product manager asks why each blocklist entry cannot carry a reason code. What do you say?
    The structure stores bits, not entries, so it can answer one question about one input you already hold and carry no payload at all. If the product needs a reason, severity or expiry, that data needs a real map — often an exact structure for the small set of entries users will actually see, with the filter only screening the long tail.

saying these in an interview costs you the question

  • Argues memory savings without pricing the false positives
  • Assumes a false positive is always cheap to confirm
  • Plans to delete entries from an already shipped filter
  • Ignores that memory still grows linearly with list size
  • Picks the error rate by feel rather than from fallback cost
  • Overlooks that public parameters let an attacker mine hits

context