Drills 16 to 18 and 34 to 36: caching, edge and frontend

Six drills, ninety seconds each, out loud. Two clusters that share a property: the naive answer is a well-known technique that does not actually solve the stated problem. Jittering TTLs does not fix a stampede on one hot key. Optimising handlers does not fix INP when the main thread is blocked. Micro-frontends do not fix a slow build.

So the shape that works here is: name the naive answer, say precisely why it fails, then give the one that works. That is a stronger move than going straight to the right answer, because it demonstrates you know why the obvious thing is obvious and wrong.


Drill 16. Prevent a cache stampede on a hot key. Three approaches.

First I'd separate the two problems, because they get conflated. Jittering TTLs across many keys prevents synchronised expiry, where a thousand keys written at the same time all expire together. It does nothing for a stampede on one hot key, because there's one expiry instant and every client hits it. That's the important distinction and it's why the obvious answer fails.

Three approaches for the single hot key.

A lease, or single-flight: the first request to find the key missing acquires a short-lived lock and regenerates; everyone else either waits briefly or serves the previous value. One origin request instead of nine hundred. The failure to design for is the lock holder dying, so the lease needs a short TTL and the waiters need a bounded wait.

Probabilistic early expiry, XFetch: each reader independently decides to refresh early with a probability that rises as expiry approaches, weighted by how expensive the recompute was. So the refresh is spread over a window before expiry rather than concentrated at it, and statistically one client refreshes while the others still get a valid value. It's elegant because it needs no coordination at all.

And stale-while-revalidate: serve the stale value immediately and refresh in the background, so nobody ever waits on the origin. This is the one I'd reach for first when the data tolerates a little staleness, because it converts the problem from a thundering herd into a slightly stale response.

The number that makes it concrete: we had a fifteen-minute TTL on a popular-products query, and at expiry roughly nine hundred requests hit the origin in the same second and took the database to a hundred percent CPU for about forty seconds. Which is why I always ask what happens at expiry rather than what the hit rate is.

Depth signal: dismantling the jitter answer precisely, and the scar-tissue number.

Full treatment: Cache stampede on a hot key.


Drill 17. Invalidate cached content with complex dependencies. How?

I wouldn't pick one strategy for the page, because the dependencies differ by orders of magnitude. The first thing I'd build is a table: for each entity, how often it changes, how stale it can be, and how many cached objects one change touches.

Concretely: price changes fifty thousand times a day, tolerates zero staleness for legal reasons, and touches one product. A category changes twenty times a day, tolerates an hour, and touches eighty thousand products. Those need different mechanisms.

So: surrogate keys for the small fan-out, where I tag each cached response with the entities it depends on and purge by tag, and the CDN finds the objects so I don't need to know which URLs they were. Versioned keys where one change touches thousands, because bumping a version is one increment instead of eighty thousand deletes, at the cost of superseded entries sitting in memory until eviction. A short TTL for the genuinely volatile fields. And a long TTL underneath everything as a backstop, because a purge is a message and messages get dropped.

Two things I'd add. Where the volatile input is cheap to render and the rest is expensive, split the fragment rather than compromising on one TTL: price is two percent of the render cost and needs ten-second freshness, the shell is the expensive part and can live an hour. And drive purges from change data capture rather than application code, because an application-emitted purge is a line someone can forget in the admin tool, the bulk importer or the manual data fix.

And invalidation and stampede control are one design, not two: stale-while-revalidate is what makes aggressive purging safe.

Depth signal: refusing the single-strategy framing, the fan-out threshold for tags versus versions, and the backstop TTL reasoning.

Full treatment: Invalidating with complex dependencies.


Drill 18. SSE or WebSocket for streaming LLM tokens, and why?

SSE, for this specific case, and the reason is that the communication is one-directional. The client sends one request and receives a stream of tokens; it doesn't need to send anything mid-stream. WebSocket buys bidirectionality that this use case doesn't use, and charges for it.

What SSE gives you: it's plain HTTP, so every proxy, load balancer, CDN and corporate firewall already handles it, whereas WebSocket's upgrade handshake is a thing that gets broken by middleboxes. Automatic reconnection with Last-Event-ID is in the protocol, so resuming a dropped stream is built in rather than something you implement. Standard HTTP auth, standard compression, standard observability, because it is a normal HTTP response.

The limitations worth knowing: over HTTP/1.1 browsers cap connections per origin at six, and a long-lived SSE stream occupies one, which matters if a page opens several. Over HTTP/2 that goes away because of multiplexing. And SSE is text-only, UTF-8, which is fine for tokens and not for binary.

When I'd choose WebSocket: genuinely bidirectional, low-latency, high-frequency communication. A collaborative editor, a multiplayer game, a trading interface. If the client needs to send messages mid-stream, SSE means a second channel and at that point WebSocket is simpler.

The operational detail for LLM streaming specifically: disable proxy buffering, because a proxy that buffers the response defeats streaming entirely and the symptom is that the whole response arrives at once after the full generation. That's X-Accel-Buffering: no on nginx, and it's the thing that catches people.

Depth signal: the HTTP/1.1 connection cap and its HTTP/2 resolution, and the proxy buffering operational detail.

Full treatment: SSE vs WebSockets for token streaming.


Drill 34. Half our Redux store is server data. What is the migration and why?

The reframe first: Redux is a client state manager, and server data isn't client state, it's a cache of something someone else owns. Treating it as client state means hand-writing everything a cache does: loading flags, error states, staleness, deduplication of concurrent requests, invalidation, refetch on focus. That's usually most of the Redux code in an application, and it's all mechanism rather than product logic.

So the migration is to a server-cache library, React Query or RTK Query or SWR, which provides those primitives, and to leave in Redux only genuine client state: UI state, form drafts, wizard progress, optimistic local edits.

The order matters. I'd migrate one feature end to end rather than one concern across the app, because a half-migrated slice where some components read from Redux and some from the cache has two sources of truth and is worse than either. Pick the feature with the most server data and the least client state, do it completely, and use it as the reference.

The measurable outcome is usually a large reduction in state-management code, because all the loading and error boilerplate disappears, and a behavioural improvement people don't expect: request deduplication and background refetch come free, so the app feels fresher without anyone writing that.

What I'd watch for is the temptation to keep a Redux copy "for convenience". That's the two sources of truth problem again, and it's how these migrations end up half done permanently.

Depth signal: "server data is a cache, not state" as the reframe, and migrating by feature rather than by concern.

Full treatment: Migrating a Redux store to a server cache.


Drill 35. Our INP is bad. Diagnose.

INP has three phases and the split is the diagnosis: input delay while the main thread is busy, processing while my handler runs, and presentation while the browser does style, layout and paint. So the first thing I'd get is field data from the web-vitals attribution build, broken into those three phases and segmented by interaction target. That table is the answer.

My prior is that input delay dominates and the cause is third-party scripts, usually a tag manager loading a dozen vendor tags. In the case I worked, the team had spent a sprint optimising handlers, which was ninety milliseconds of an eight-hundred-and-ninety millisecond problem.

If processing dominates, the highest-leverage fix isn't making the handler faster, it's painting the feedback first: set the pending state, yield to let the browser paint, then do the work. Because INP measures time to the next paint rather than time to complete the work, that alone can take an interaction from four hundred milliseconds to thirty without anything actually getting faster.

If presentation dominates, it's DOM size and style recalculation, and that one is invisible in a JavaScript profiler, which is why it gets missed. The fixes are virtualisation for long lists and content-visibility: auto for off-screen sections, not code changes.

And I'd insist on segmenting by interaction target rather than looking at the aggregate, because different interactions usually have different causes and one aggregate number leads to one fix that moves nothing.

Depth signal: the three-phase split as the method, the paint-first reframe, and presentation delay being invisible in a JS profiler.

Full treatment: Diagnosing a bad INP.


Drill 36. When are micro-frontends worth it?

The test I'd apply is whether the teams' work co-renders on the same page. If teams own separate routes, split by route: you get real deploy independence with none of the shared runtime problems, because two routes never render at the same time. If teams own separate widgets on one page, that's the genuine micro-frontend case and Module Federation earns its cost.

But before either, I'd check what the measured pain actually is. In most organisations that raise this, it's merge queue depth and a shared release train, and affected-only builds plus trunk-based development with feature flags fix both in about nine weeks against six months for full runtime composition. Recommending an architecture change for a pipeline problem is the specific failure mode here.

The hard part, if you do it, is the shared dependency singleton. singleton: true means one React across all fragments, so every team upgrades together, which is exactly the coordination you were trying to remove. Drop the singleton and you ship two React copies and hooks break across the boundary, because two React instances don't share the internal dispatcher. There's no clean answer, and that's usually what decides it.

Thresholds: below about five teams, no. Above ten with genuinely co-rendering ownership, yes. In between, split by route and fix the pipeline. And I'd put a byte budget in CI from day one, because bundle duplication of thirty to fifty percent is commonly reported and it lands on mobile users first.

Depth signal: the co-rendering test, the pipeline reframe with numbers, and the singleton contradiction.

Full treatment: When micro-frontends are worth it.


How to practise these

These six share a practice technique that the other drill batches do not need: rehearse the dismantling of the obvious answer.

For each one, the naive response is a real technique that a reasonable person would offer, and the value of your answer is in saying precisely why it does not apply here:

Drill 16   "jitter the TTLs"        -> fixes synchronised expiry across
                                       MANY keys, does nothing for ONE
                                       hot key
Drill 17   "shorter TTL"            -> a bound on wrongness, not a
                                       mechanism for correctness, and
                                       the hit rate collapses
Drill 18   "WebSocket, it's more
            capable"                -> buys bidirectionality this use
                                       case never uses, and costs
                                       middlebox compatibility
Drill 34   "put it all in Redux"    -> hand-writes everything a cache
                                       already does
Drill 35   "optimise the handler"   -> often the smallest of the three
                                       phases
Drill 36   "micro-frontends"        -> usually a pipeline problem
                                       wearing an architecture costume

Three tests for your own answer:

  1. Did you name the obvious answer and why it fails? Doing this first is more persuasive than going straight to the correct one, because it shows you understand the shape of the problem rather than having memorised a solution.
  2. Did you give a threshold rather than a preference? "Below five teams, no; above ten with co-rendering, yes" is a position. "It depends on the situation" is not, and the whole point of these drills is having a defensible line.
  3. Did you include the operational detail? Proxy buffering for SSE. Restoring refresh_interval before the alias swap. The byte budget in CI. Those are what distinguish someone who shipped it from someone who evaluated it.