Dev Skills
← Search typeahead·Worked solution

Search typeahead — a full worked answer

The whole thing, start to finish, in the order you would actually say it. Read it to learn the shape of a strong answer — then go and produce one yourself, because reading is not the same skill as producing.

core
Common trap
Read this second, not first. Reading a good answer feels like learning and only builds recognition. If you have not tried this problem yet, close this page and write your own answer first. It will feel worse and it is the part that works.

The question

Design search suggestions that appear as the user types. Every keystroke should produce updated suggestions, ranked by what people actually search for.

What kind of problem this is

Search and ranking. The user gives you a fragment and expects the right answer ranked first.

Two systems: one that narrows millions to dozens, one that ranks the dozens. Confusing them is a common mistake.

Before anything: where is the slack?

How long the system has before a person notices. Ask this before you draw a single box — it decides more of the design than scale does.

Per keystroke: ~100 ms. Index freshness: hours.

A hundred milliseconds per keystroke, because a person types the next character in about 200. And hours of slack on how fresh the suggestions are.

The latency budget is set by typing speed. A moderate typist produces a character every 150 to 250 ms, so a suggestion arriving after 300 ms is already answering a question the user has moved past. Above about 100 ms, the list visibly lags behind the cursor. That budget rules out almost any approach that searches at request time.

The freshness budget, by contrast, is huge, and this is the asymmetry to exploit. Suggestions come from what people searched for in the past, and yesterday's popularity predicts today's very well. An index rebuilt once an hour, or even once a day, gives results that look the same as real-time ones to almost every user. So you have hours to precompute whatever you need.

The exception is trending: a breaking news event makes a term explode in minutes, and suggestions that ignore it feel broken. That is a narrow, high-value exception. It calls for a small fast-moving layer over a slow-moving base, not for making the whole system real-time.

Hours of build slack plus 100 ms of serve budget makes this one of the clearest precompute-versus-compute-on-read decisions on the site.

So: You precompute the answer for every prefix ahead of time and serve it from memory. You do not search at request time. The only live component is a small trending overlay.

1

Requirements

about 5 min

Why this stage exists: Because the question is deliberately vague, and every box you draw before this is a guess.

What you are being asked for: State your assumptions, ask one or two questions that change a box, and propose the scope.
What a strong answer sounds like

Assumptions: global, and suggestions come from historical search queries rather than from a product catalogue: the top queries starting with what has been typed. One search generates several requests, one per keystroke, so request volume is many times the search volume. Suggestions are short strings, so the data is small. Users type on mobile keyboards, with typos.

First question, and it decides whether the answer can be shared: are suggestions personalised, or the same for everyone typing the same prefix? Global suggestions can be precomputed once and cached at the edge for everybody. Personalised ones mean a per-user computation inside a 100 ms budget, and no shared caching. I will assume global, with light personalisation applied on the client from local history.

Second question: do we need to handle typos and near-misses, or exact prefixes only? Fuzzy matching is a much more expensive lookup and would change the data structure. I will assume exact prefix matching first, with a limited typo tolerance as a second layer.

Scope: return ranked suggestions for a prefix, keep them reasonably fresh, and handle trending terms. Out of scope: the actual search results page, spelling correction on submitted queries, and multi-language handling, though I would flag the last as a real complication.

The costs named here
Global suggestions, personalised on the client → costs: Loses server-side personalisation quality. In exchange every response is shareable and cacheable, which is what makes a 100 ms budget achievable at scale.
Say this
"Assuming suggestions are global (the same prefix gives the same list for everyone), with personalisation applied client-side from local history. That assumption is what lets me cache everything at the edge. If they had to be personalised per user, this becomes a different and much harder system, so it is worth asking."
Common trap
Not noticing that one search means five to ten requests. The request rate is not the search rate, and capacity planning starts from that multiplier.
What an interviewer is ticking off in this stage
  • Stated that suggestions come from historical queries, not a catalogue
  • Noted request volume is a multiple of search volume — one per keystroke
  • Asked global versus personalised and named the caching consequence
  • Asked about typo tolerance and deferred it as a second layer
  • Proposed a scope and excluded the results page
2

Actors and lifecycle

about 6 min

Why this stage exists: Because the happy path is the easy half. The failure branches are where the design actually lives.

What you are being asked for: List the actors — including the system — then write the life of a suggestion as a chain, and add the failure branches.
What a strong answer sounds like

Actors: the person typing, the population of past searchers whose behaviour creates the rankings, and the system, which decides what qualifies as a suggestion, what to suppress, and what counts as trending.

The chain: queries logged → aggregated by frequency → filtered and ranked → built into the prefix structure → served for a prefix → eventually decays out of the rankings.

Failure branches. At logged: bot traffic inflates a query's count, so rankings reflect scripts rather than people. The system must filter obvious automation, or suggestions get poisoned. At filtered: an offensive or harmful query becomes popular, and because suggestions come from raw behaviour, the system will promote it. This is a requirement, not a nicety: it needs a blocklist and a review path, and candidates often forget it. At built: the index build job fails, so suggestions silently go stale. A slightly old index looks normal, so this needs explicit monitoring on index age.

At served: the responses arrive out of order. The user typed "ca", then "cat", and the "ca" response arrives second, overwriting the more specific suggestions with less specific ones. This is a common bug, and the fix belongs in the design: every response carries the prefix it answers, and the client discards anything that does not match the current input.

At served: the user types faster than the round trip, generating requests for prefixes they have already moved past. The client should debounce briefly and cancel in-flight requests.

At served: no suggestions match at all. Return nothing cleanly rather than falling back to unrelated popular queries, which feels broken.

And the decay branch: a query that was hugely popular last month should fade, so ranking must weight recency rather than all-time totals, or the suggestions become a museum.

Common trap
Ignoring out-of-order responses. Users report it as "the suggestions flicker and show the wrong thing". The cause is in the request protocol, so the fix belongs in the design, not in a UI patch.
What an interviewer is ticking off in this stage
  • Traced the suggestion from logged queries to being served
  • Handled bot traffic poisoning the rankings
  • Handled offensive suggestions — blocklist and review path
  • Handled a failed index build, and noted the failure is silent
  • Handled out-of-order responses by matching prefix to current input
  • Debounced and cancelled in-flight requests on the client
  • Handled the empty result case cleanly
  • Weighted ranking by recency so old queries decay

The lifecycle, drawn

Queries loggedsearchersAggregatedbatch jobRanked + filteredsystemIndex builtsystemServedtypistBot traffic inflates countsfilter automation before aggregatingOffensive query trendsblocklist at build time AND at serve timeBuild job fails silentlyatomic pointer switch keeps the old index; alert on index ageResponses arrive out of orderresponse carries its prefix; client discards mismatchesUser types faster than the round tripdebounce ~50 ms and cancel superseded requestsNo matches at allreturn nothing cleanly — never unrelated popular queries
A suggestion, from logged queries to a dropdown. The out-of-order response branch is a design problem that shows up as a UI bug.
3

Numbers

about 6 min

Why this stage exists: Because one part of this is hard and the rest is easy, and they look identical until you do the arithmetic.

What you are being asked for: Estimate request rate, index size, and build cost. Then finish "So the hard part here is ___."
What a strong answer sounds like

Requests: assume 100,000 searches per second at peak. At 6 keystrokes per search with debouncing, that is around 600,000 suggestion requests per second. That is a high request rate for a 100 ms budget, and it rules out any expensive work per request.

Distinct queries: people search for a huge variety of things, but the tail is worthless as suggestions. A query searched twice ever should not be suggested. Take the top 10 million queries, which covers nearly everything worth suggesting.

Index size: 10 million queries at roughly 40 bytes each is 400 MB of raw text. Storing the top 10 completions at every prefix node inflates that; call it a few gigabytes with the structure and rankings. That fits in memory on one machine, and this conclusion shapes the design: the suggestion index can be copied onto every serving node rather than sharded and queried over a network.

Build cost: aggregating a day of query logs, maybe 10 billion entries, is a batch job of tens of minutes on a modest cluster. That fits easily inside hours of slack.

Cache effectiveness: prefix popularity is highly concentrated. The top few thousand prefixes account for a large share of all requests, because everyone types the same first two or three characters. So a small cache absorbs most traffic, and short prefixes — the most requested — are also the most cacheable.

So the hard part here is request rate against a 100 ms budget, and the answer is precomputation plus caching. Not index size, not build cost, and not the ranking algorithm.

Say this
"Six hundred thousand requests a second, and the whole index is a few gigabytes, so it fits in memory on one machine. So the hard part is request rate against a hundred-millisecond budget. The answer is to precompute everything and replicate the index to every serving node, with no network lookup on the hot path."
Common trap
Trying to suggest from all distinct queries. The tail is huge and useless: a query one person searched once is not a useful suggestion. Bounding to the top N is what makes the index fit in memory, and that changes the design.
What an interviewer is ticking off in this stage
  • Applied the keystroke multiplier to get the real request rate
  • Bounded the index to the top N queries and justified dropping the tail
  • Concluded the whole index fits in memory on one machine
  • Noted the build is a batch job that fits comfortably in the slack
  • Noted prefix popularity is concentrated, making caching very effective
  • Finished the sentence: request rate against a tight latency budget

The numbers, drawn

Searches~100,000 / sec
Suggestion requests (6 keystrokes each)~600,000 / sec
Index size (top 10M queries)~3 GB — fits in memory

The index fits on one machine, so replicate it to every serving node and take the network out of the hot path entirely. So the hard part is request rate against a 100 ms budget, which precomputation solves and per-request search does not.

A huge request rate against a tiny budget, and an index small enough to hold everywhere.
4

High-level design

about 12 min

Why this stage exists: Because now — and only now — you have a reason for every box you are about to draw.

What you are being asked for: Draw the design. Justify each box, and be specific about the data structure.
What a strong answer sounds like

The data structure is the design. A trie over the top 10 million queries, where each node stores the top 10 completions beneath it, precomputed at build time. Answering a request is then a walk down the prefix, one step per character, and a read of a list that is already there. No search, no ranking, no scoring at request time. Justified directly by the 100 ms budget and the 600,000-per-second rate: anything computed per request would not fit.

That precomputation is the central trade. Building it is expensive and takes hours; serving from it is close to free. With hours of slack on freshness and 100 ms on serving, that is the right way round.

Ranking happens at build time: score each query by frequency weighted toward recent days so popularity decays, apply blocklists, and store the ordered top 10 at each node. Justified by the Stage 2 branches on decay and offensive content, which can both be handled at build time.

Serving: the whole index is replicated to every serving node, held in memory. Nodes are stateless with respect to each other and scale horizontally behind a load balancer. Justified by the size estimate: a few gigabytes per node is cheap, and it removes any network hop from the lookup.

Caching: an edge cache in front, keyed by prefix, with a TTL of minutes. Because short prefixes dominate traffic and are identical for everyone, the hit rate is very high, and most requests never reach a serving node. This box saves more work than any other, justified by the concentration observed in Stage 3.

Index building: a batch job reads query logs, aggregates, filters, ranks, and produces a new index artefact. Deployed by building the new index alongside the old one and switching an atomic pointer. Nothing is mutated in place, so there is no window where the index is half-updated. Justified by the Stage 2 branch about failed builds: with an atomic switch, a failed build leaves the previous index serving.

Trending overlay: a small, separately-updated layer of terms rising sharply in the last few minutes, merged into results at serve time. Kept small and separate on purpose, so the fast-moving part does not force the whole system to be real-time. Justified by the carve-out in the slack analysis.

Client behaviour is part of the design: debounce by about 50 ms, cancel superseded requests, discard responses whose prefix does not match the current input, and cache locally so backspacing does not re-request. That last one matters more than it sounds. Backspacing is common, and the client has already seen the response for every shorter prefix.

The costs named here
Precompute top-N at every trie node → costs: Much larger index and an expensive build. Buys a lookup with no ranking work at request time, which is the only way to hit the budget.
Replicate the full index to every node → costs: Every node carries a few gigabytes and a full rebuild is deployed everywhere. Removes all network lookups from the hot path.
Hourly rebuilds instead of real-time → costs: New queries take up to an hour to become suggestable. Invisible for almost everything, which is why trending gets its own small path.
Common trap
Querying a general-purpose search engine per keystroke. It is built for a different job (scoring documents against a query), and holding 100 ms at 600,000 requests a second would take a very large cluster. Precomputation is the answer here, and knowing when not to use the search engine is what the interviewer is looking for.
What an interviewer is ticking off in this stage
  • Chose a trie with precomputed top-N at each node, and justified it with the latency budget
  • All ranking done at build time, nothing at request time
  • Index replicated to every node and held in memory
  • Edge cache keyed by prefix, justified by prefix concentration
  • Index deployed by atomic pointer switch, so a failed build is safe
  • Trending handled as a small separate overlay
  • Client-side debounce, cancellation, prefix matching and local cache

The architecture, drawn

atomic swapmergemissQuery logsBuild jobaggregate, rank, filterIndex artefacttrie, top-10 per nodeTrendingrefreshed ~1 minTypistEdge cacheby prefixSuggest svcindex in memory
The expensive work happens hourly, offline. The request path is a prefix walk in memory, and most requests are answered by the edge cache before they get that far.
UserCachehas itDatabase
request inanswer back — the database never woke up
Short prefixes are requested constantly and are identical for everyone, so the edge answers almost all of them. Most requests never reach a serving node.
5

Deep dive and tradeoffs

about 14 min

Why this stage exists: Because naming what a decision costs is the single clearest signal of seniority there is.

What you are being asked for: Go deep on the two hardest parts. Name a cost after every decision.
What a strong answer sounds like

Hard part one: freshness against precomputation, the main tension in this design. The base index rebuilds hourly, which is fine for the long tail. Trending is the exception, so I keep a separate small structure updated every minute or two from a stream of recent queries, holding terms whose rate has risen sharply. At serve time the two are merged, with trending items boosted. Keeping this small (thousands of terms, not millions) is what makes it affordable. Cost: two ranking sources merged at serve time, so the ordering logic is more complex and the results are harder to explain when someone asks why a suggestion appeared.

Deploying a new index has a memory cost worth naming: to switch atomically you hold both the old and new index in memory briefly, so peak memory is roughly double steady state. If nodes are not sized for it, the swap itself can run a node out of memory. Cost: sizing every node for 2x, or accepting a brief window of reduced capacity while nodes rebuild in rotation.

Hard part two: typo tolerance without abandoning the structure. A trie is exact by nature. Options: precompute common misspellings as additional entries pointing at the correct query, which is cheap and covers most real typos because they cluster heavily; or fall back to a fuzzy search only when the exact prefix returns too few results, which is slower but rare. I would take both: precomputed misspellings for the common cases, fuzzy fallback when the exact lookup comes back nearly empty. Cost: a larger index, and the fallback path has a much worse latency profile, so it needs a tight timeout and must degrade to returning nothing rather than blowing the budget.

Hot prefix protection: single-character prefixes are requested constantly. Those are cache hits at the edge, so they rarely reach a serving node. I would give the shortest prefixes long TTLs, since their answers barely change. Cost: a trending term takes slightly longer to appear under a single-letter prefix, which users are unlikely to notice.

Abuse and rate limiting: an automated client can generate enormous request volume cheaply here, since every keystroke is a request. Per-client rate limiting is needed here, because without it one script can consume capacity meant for real typists. Cost: an aggressive typist on a shared network could hit a limit, so the limit must be generous and keyed carefully.

Multi-language, raised because it complicates this a lot in practice: tokenisation and prefix semantics differ by script. In languages written without spaces (Chinese, Japanese, Thai), or typed through an input method, "prefix" does not mean the same thing. I would build per-language indexes and route by locale rather than pretend one structure handles everything. Cost: N indexes to build and hold, and ambiguity for multilingual users.

Consistency per feature: suggestions are eventually consistent by up to an hour and that is by design. Trending is eventually consistent by a minute or two. Blocklists must apply immediately, which means they are checked at serve time as well as at build time. A filter that only runs at build time leaves an offensive suggestion live until the next rebuild.

What I would monitor: p99 latency per request, cache hit rate by prefix length, index age (the silent failure), trending pipeline lag, and the rate of empty-result responses, which is the earliest signal that an index build produced something wrong.

The costs named here
Separate trending layer → costs: Two sources merged at serve time, harder to explain and to tune. Avoids making the entire pipeline real-time for a small slice of terms.
Fuzzy fallback for near-empty results → costs: A much slower path, so it needs a tight timeout and must return nothing rather than exceed the budget.
Blocklist enforced at serve time → costs: A check on every request, on the tightest budget in the system. Required, because the build-time-only alternative leaves bad suggestions live for up to an hour.
Say this
"Everything is precomputed at build time and I do no ranking per request. Blocklists are the exception: they are checked at serve time as well, because a build-time-only filter would leave an offensive suggestion live until the next rebuild. Cost: one extra check on my tightest latency path, which I accept because the alternative is up to an hour of exposure."
Common trap
Treating freshness as a single number for the whole system. The base index can be hours old, trending must be minutes, and blocklists must be immediate. Three different budgets, three different mechanisms.
What an interviewer is ticking off in this stage
  • Trending handled as a small separate structure merged at serve time
  • Noted the 2x memory cost of an atomic index swap
  • Typo tolerance via precomputed misspellings plus a bounded fuzzy fallback
  • Hot short prefixes handled by caching with long TTLs
  • Rate limiting justified by the per-keystroke request pattern
  • Raised multi-language as a real complication with a real answer
  • Blocklists enforced at serve time, not only at build time
  • Named what to monitor, including index age and empty-result rate
  • Cost named after every decision

The decision this turns on

A
Precomputed trie, top-N per node
  • —A request is a short prefix walk plus a list read: microseconds.
  • —All ranking, filtering and decay happen offline where time is free.
  • —Index is a few GB, so every node can hold the whole thing.
  • —New queries are invisible until the next rebuild.
B
Query a search engine per keystroke
  • —Fresh immediately, and fuzzy matching comes for free.
  • —No build pipeline, no artefact management, no atomic swaps.
  • —Scoring per request will not hold 100 ms at 600,000 requests/sec.
  • —Cost scales with request volume rather than with data size.
Which one: Precompute. Hours of freshness slack and 100 ms of serving budget is the textbook signal to move work from read time to build time. Add a small trending layer for the one case where hours is too slow, rather than making everything real-time to serve a fraction of the terms.
Precompute at build time, or search at request time. The budgets decide it.

And then they push back

A complete answer is where the interview starts, not where it ends. These are the questions this design invites — the interview is decided here far more often than in the diagram.

10x traffic tomorrow. What fails first?
A weak answer

"We would add more servers and scale horizontally." This says nothing. Every system has a first thing to break, and not naming it means you do not know your own design well enough to have found it.

A strong answer

Name a specific component and the reason. "The stateless services scale out fine, so they are not the constraint. The database primary is, because every write goes through one machine and I am at roughly 40% of its write capacity now. So at 4x I am at the ceiling, and I would see write latency climb and connection pool waits before anything errors. The second thing is the cache, because 10x reads on a fixed working set is fine but 10x on a growing key space means my hit rate drops and the misses land on the database that is already the constraint. In order: database writes, then cache hit rate, then the connection pool. And I would find out from p99 write latency, not from errors."

The cache just died. What happens?
A weak answer

"Requests would go to the database instead, so it would be a bit slower." At the ratios that justify a cache, the database cannot take that load. This answer means you added a cache without checking what it was protecting you from.

A strong answer

"Every read that was being served from memory lands on the database at once. My cache was absorbing something like 99% of reads, so the database sees roughly 100x its normal load, and it will not survive that. A naive design goes down. What I would do about it, designed in advance: coalesce misses so a thousand simultaneous requests for the same key produce one database read rather than a thousand; keep a small in-process cache on each server as a second layer so the hottest keys never leave the machine; shed load at the edge rather than let the database die, because a degraded site beats a dead one; and bring the cache back warm rather than empty, because restarting into an empty cache reproduces the outage. Cost of the in-process layer: servers can briefly disagree with each other."

One user reports a slow request. How do you debug it?
A weak answer

"I would check the logs." Which logs, for what? At scale, "check the logs" without a way to find that specific request is a way of saying you have not thought about observability.

A strong answer

"Work down rather than guessing. First: is it slow for everyone or just them? A dashboard of that endpoint's p50 and p99 answers it in seconds — if p99 is bad and p50 is fine, it is probably data-dependent, meaning that user has far more rows than the median. Second: get a trace of their actual request, which requires request ids propagated through every service, and see whether the time went to one slow call or two hundred fast ones. Two hundred fast ones is an N+1, which is invisible in development and common in production. Third: if it is one slow query, run EXPLAIN with their parameters, not typical ones, because the plan can differ once their row count crosses a threshold. The common outcome is that they are a heavy account and the query has no index for their filter combination. What makes this possible at all is having decided beforehand to propagate request ids and to log with enough structure to find one user's request among billions."

Why this database and not Postgres?
A weak answer

"It scales better and is designed for high throughput." Vague and unfalsifiable. Postgres handles far more than most people assume, and an answer with no number in it sounds like a preference.

A strong answer

"The answer has to be a number or an access pattern. Something like: the write rate is around 80,000 a second sustained on one logical table, append-only, with no cross-row transactions. That is more than I would plan to commit on one Postgres primary, so I would be sharding it myself and hand-building rebalancing and failover that a distributed store already has. The read pattern is a single-key lookup, so I am giving up joins I do not need. And then what it costs: no cross-partition transactions, so any invariant spanning partitions moves into application code, and analytics needs a separate copy of the data. If I cannot produce a sentence like that, then the correct answer is that Postgres is fine. Saying so is the stronger answer: choosing the boring option on purpose shows more judgement than reaching for the distributed one."

Now make it multi-region.
A weak answer

"We would replicate the database to another region." That covers reads. The question is what happens to writes, and that is where the design actually changes.

A strong answer

"Reads are the easy half: replicas in each region, users read locally, and they may be slightly stale — I would say how stale and which reads cannot tolerate it. Writes are the hard part, and there are three options. Single write region: simplest, no conflicts, and users far away pay 150 ms per write, which is fine if writes are rare and unacceptable if they are not. A home region per data partition, so each user's data lives near them: local writes without conflicts, but it needs a routing layer plus a plan for users who move. Multi-leader with conflict resolution: local writes everywhere, and now I own a conflict problem, which is only acceptable where merges are safe. I would pick the second for user-owned data and single-region for anything globally contested like inventory. What changes elsewhere: cross-region replication cost, a failover story per region, and data residency rules that may force certain data to stay in certain places. I would ask about that constraint rather than discover it later."

What does this cost per month?
A weak answer

"I am not sure, it depends on the provider." Interviewers rarely want exact prices. They want to know whether you understand which line item dominates, and that is answerable from your own estimates.

A strong answer

"I would not guess at prices, but I can tell you the shape, which is what matters. The dominant cost here is egress bandwidth: my estimate said 1,350 GB a second to viewers, and at that volume nothing else on the bill comes close. Second is storage, because I keep raw data for two years and it grows every day whether anyone reads it or not. Third is compute, which is elastic and therefore the easiest to control. That ordering tells me where to spend engineering effort: a percentage point of CDN hit rate is worth more than any amount of compute optimisation, so cache hit rate is the number I would put on a dashboard and defend. Storage cost I would attack with lifecycle policies moving cold data to cheaper tiers and with tiered granularity. Compute I would run on interruptible capacity because the work has hours of slack."

How do you deploy a new version with no downtime?
A weak answer

"Rolling deploy behind the load balancer." Correct and incomplete. The hard part is database changes and in-flight requests, and stopping there suggests you have only done it on a stateless service.

A strong answer

"Rolling deploy, taking instances out of the pool, draining in-flight requests (skip connection draining and you drop live traffic during a zero-downtime deploy), then updating, waiting for health checks, and putting them back, keeping enough capacity in the pool throughout. The part that makes it work is that the new version must tolerate the old version running beside it, because for several minutes both are live. That means database migrations get split: add the new column, deploy code that writes both and reads the old, backfill, deploy code that reads the new, then remove the old column. That is three or four releases instead of one. Cost: every schema change is now a multi-deploy sequence, which is slower and is the price of not taking downtime. And I would rather roll forward to a known-good build than roll back, because undoing a migration is much harder than undoing code."

Replica lag is 30 seconds and a user reads their own write. What do they see?
A weak answer

"They might see slightly old data." They see their own comment missing, which they will report as data loss. Understating this suggests you have not watched users hit it.

A strong answer

"They see the state from before their write, and they will report it as the app losing their work. This is the read-your-own-writes problem, and users notice it because it is their own data. The fix is not to make everything strongly consistent, which would slow every read for everyone. It is to route that user's reads to the leader for a short window after they write, or to have the client pass the write position it saw and let the read wait for a replica that has caught up. Cheapest of all for this specific case: render their own item from what the client already has and do not re-fetch. Cost of leader-pinning: my most active users generate the most leader load, which is a real but bounded price. And separately, 30 seconds of lag is itself a symptom — usually a long transaction or a bulk write hogging the leader — so I would alert on it rather than only design around it."

The topics this answer used

Now do it yourself

You have read a good answer. That is worth very little until you can produce one on a blank page — so go and write this problem without looking, and compare afterwards.