Sample report

Sample data for a fictional student, from a scripted interview. The report was written by the same pipeline that writes yours.

System design interview

Software Engineering, regular level, one design on the whiteboard, Oct 16, 2026

Strong hire

If you read nothing else

Above the Regular bar in this interview.

  • Strength: Requirements before design: you asked five sharp scoping questions and wrote back quantified functional and non-functional requirements, including the fail-open preference that later shaped every failure decision.

    Non-functional: under five milliseconds added, a million checks a second, highly availableYour words, 3:09
  • Strength: Credible depth on the critical component: the atomic token bucket, its Lua implementation, clock-skew handling, sharding and slot maps, all explained without hand-waving.

    the read, refill and decrement have to be one atomic step. In Redis I'd use a Lua script, which runs atomically on the shardYour words, 11:07
  • Gap: Design config-change safety, not just rollback.

    I guess we'd roll it back quickly? Maybe someone reviews rule changes before they go out.Your words, 28:22

One moment from your delivery

You hesitated and used hedging phrases

When asked “What if someone pushes a bad rule that blocks all traffic?” (Q1), you hesitated and used hedging phrases: you took 7.2 s to start (your usual 3.5 s); a 3.3 s pause mid-answer; your pitch went up at the end of 2 of 2 phrases; 2 hedges such as 'I guess'.

  • Time to start 7.2 s vs your usual 3.5 s
  • Longest pause 3.3 s
  • Your pitch went up at phrase ends 2 of 2
  • Hedging 2 (“I guess”, “Maybe”)
I guess we'd roll it back quickly?Your words, 28:22

Try: Start with a one-line headline ('My approach is X because Y') and think aloud from there instead of in silence.

Your next task, 20 min

Design config-change safety, not just rollback

Write and talk through a one-page safe rule-rollout pipeline for your limiter: validation, shadow, canary, automatic rollback trigger, owner of the page. Then say it out loud in 90 seconds.

What happened

You designed a distributed rate limiter: you gathered requirements and numbers, estimated state size and shard counts, then moved from a central service to a sidecar over sharded Redis with an atomic token bucket. You handled hot keys, shard failure, daily quotas and burst-from-many-IPs, and named your own weak spots. Thinner areas were resharding and safe rule rollout. Your closing questions were good.

Above the bar

Most of this sits at the Regular bar: clear requirements, a coherent sidecar design and honest failure reasoning; below it, resharding behaviour and rule-change safety stayed half answers, and some bounds only appeared after the interviewer pushed.

You answered the planned question.

After that

More ways to practice, in order of impact.

  1. 1

    Bound the worst case yourself, with numbers: Do it in 15 minutes without looking back at the transcript.

  2. 2

    When a question catches you out, say what kind of answer is coming before the detail, rather than opening with a hedge; that turn had your highest hedge count.

Scorecard

Each skill from the rubric, scored 1 to 5, with the moments it was based on.

  • Nothing to change here — you converted the brief into quantified functional and non-functional requirements and stated your assumptions before designing. Keep doing exactly this.

    What are the limits keyed on: per user, per API key, per IP, something else?Your words, 1:44
    is it worse to let a few extra requests through, or to block good traffic?Your words, 2:31

    What good looks like: You'd ask those questions and then state every assumption back in one list, including ones nobody confirmed, such as expected rule count and whether decisions must be auditable per request.

    Before drawing, write a numbered requirements list and add one line of assumptions you could not confirm, then read it back.

  • You owned the agenda and used the interviewer's questions as prompts rather than as a structure. Strong.

    Non-functional: under five milliseconds added, a million checks a second, highly availableYour words, 3:09
    Okay, so my first idea, the simplest thing: every API calls a central rate limiter serviceYour words, 5:27

    What good looks like: You'd set your own agenda out loud (requirements, capacity, API, components, then failure and scale) and say which part you are moving to next instead of following the interviewer's prompts.

    To reach 5: owns the agenda and manages time.

    Say your plan in one sentence at the start and announce each section as you enter it, so the interviewer follows your map.

  • Precise, and both decisions (cost as an input, rules outside the store) were justified by how the data is accessed. No change needed.

    It returns allowed or not, the remaining tokens, and a retry-after in millisecondsYour words, 5:57
    A rule has an ID, a scope, like per user on endpoint X, a rate and a capacity.Your words, 13:07

    What good looks like: You'd also say why the caller sends raw attributes rather than a rule id, what happens with cost greater than capacity, and how the key encodes the rule version so old and new rules don't share buckets.

    Spell out one concrete example key and value end to end, plus the error and edge cases for the check call.

  • Coherent, simple, and each piece traces back to a stated requirement. Good.

    A sidecar is one implementation, and we can roll it out ourselves.Your words, 8:10
    I'd send them in parallel, so latency is the slowest one, not the sum.Your words, 15:34

    What good looks like: You'd draw the whole path once (API, sidecar, Redis cluster, rules service, metrics) and show where each of the stated requirements is met on that diagram.

    Draw one labelled diagram early with every component and the request path, then point at it when answering follow-ups.

  • You explain internals credibly across several components instead of naming them. Close the one gap: know what happens to in-flight requests during a slot migration, or at least the mitigation (retry on MOVED/ASK).

    I'd use the Redis server time inside the script, so one clock per key.Your words, 11:41
    What happens to in-flight requests during resharding, honestly, I don't know the details.Your words, 14:48

    What good looks like: You'd explain resharding and slot migration with the same care as the Lua script: what a sidecar sees during a slot move and which retries are safe for a rate limit check.

    To reach 5: explains internals of several components credibly.

    Read up on Redis Cluster slot migration, then rehearse a sixty-second explanation of what happens to in-flight checks during a move.

  • Your trade-offs are consistently weighed against the stated requirements, not recited. The exception was rule rollout, where you only had a half-answer — treat a risky config change as a design problem with its own trade-offs, not an afterthought.

    Memcached is just as fast but has no scripting, so I'd need compare-and-swap loopsYour words, 13:58
    I'd still go with the bucket because teams think in 'a hundred a second with bursts of two hundred'Your words, 10:13

    What good looks like: You'd tie each choice back to a stated requirement explicitly, for example saying that partial denial from parallel checks is acceptable because over-admission beats blocking good traffic.

    To reach 5: weighs alternatives against requirements and picks with reasons.

    After each choice, add one sentence naming the requirement it serves and the cost you accept.

  • You reason through failures, hot spots and degraded modes rather than ignoring them, which is beyond the basic bottleneck. The gap is quantification from your own head: when you propose approximate local limits, derive the error bound immediately instead of waiting for numbers to be handed to you.

    it fails open for them, but keeps a local bucket as a rough limitYour words, 22:26
    Then the error is bounded by about one sync interval of the global rate, so about a hundred extra per interval.Your words, 20:16

    What good looks like: You'd do that bounding un prompted, and walk through the hot-key, shard-failure and rule-push cases in one pass without the interviewer steering you to each one.

    To reach 5: reasons through failures, consistency, hot spots and degraded modes.

    Practise failure walkthroughs un prompted: say the failure, the detection, the fallback and the user-visible effect in four sentences.

  • Clear and organised almost throughout. Watch for over-diving below the level being discussed; when you notice you're deep in internals nobody asked about, name the level boundary in one sentence and stop.

    So the median is under a millisecond and the tail is what matters.Your words, 18:00
    I haven't thought about how you'd do that cleanly, it's a bit vague.Your words, 28:45

    What good looks like: You'd keep saying where you are in your plan and close weak answers with a concrete next step, instead of ending on a hedge such as saying it is a bit vague.

    To reach 5: narrates decisions, concise and organized.

    When an answer is thin, finish with one concrete option you would try next, then pause rather than trailing off.

Also went well

  • Capacity reasoning with real arithmetic: state size, operations per second, shard count, and an end-to-end latency budget that separated median from p99 and set a timeout to protect the tail.

Also to improve

  • Deriving bounds yourself. When you proposed local buckets you could not say how much they could over-admit until the interviewer gave you a limit and a sidecar count; that quantification should come unprompted with the proposal.
  • Resharding behaviour. "I don't know the details" on in-flight requests during slot migration is the one place where depth stopped at the boundary of what you'd used before.
  • Safe configuration change. On the bad-rule question your first answer was review and rollback, which you then correctly realised doesn't help; validation plus a gradual rollout only appeared after prompting and stayed vague.

Red flags

  • Needed interviewer-supplied numbers to bound a trade-off you had introduced yourself (turn 55-57).
  • Left the rule-rollout failure mode as "a bit vague" even after recovering the main idea (turn 79).

Question by question

Open a question to see which key points you covered, in your own words, and what a stronger answer sounds like.

  1. Your answer: You asked about keys, scale, failure preference and regions first, then designed a sidecar limiter in front of each API, backed by a sharded Redis cluster running an atomic Lua token-bucket script. Rules live in sidecar memory, indexed by endpoint, and you covered hot keys, shard failure, bad rules and multi-region with local buckets and fail-open.

    Key points

    • Token bucket over fixed window; sliding counter also fine (Covered)

      So a token bucket: per key we store the tokens left and the last refill time, and refill on every read based on the time that passed.Your words, 9:24
    • Sidecar per API instance talking to shared store (Covered)

      I'd rather make it a sidecar next to each API instance: no extra network hop, and the sidecar talks to Redis directly.Your words, 7:35
    • Sharded store, atomic script, hot key handled locally (Covered)

      the read, refill and decrement have to be one atomic step. In Redis I'd use a Lua script, which runs atomically on the shardYour words, 11:07
    • Local share plus periodic sync bounds the error (Covered)

      the error is bounded by about one sync interval of the global rate, so about a hundred extra per intervalYour words, 20:16
    • Fail open with local bucket; rules pulled and cached (Covered)

      Since blocking good traffic is worse, it fails open for them, but keeps a local bucket as a rough limitYour words, 22:26

    A stronger answer

    Before drawing anything I'd pin the requirements down: rules keyed on API key, user, IP and endpoint, editable without a deploy, one million decisions a second, under five milliseconds added, and over-admission preferred to blocking good traffic. I'd put the limiter in a sidecar next to each API instance, so there's no extra network hop and no single point of failure, and the sidecar keeps token buckets in a sharded Redis cluster, around sixteen shards for a million operations a second. Each check is one Lua script that refills from the Redis server clock, takes the cost, and returns the remaining tokens and a retry-after, with a TTL so idle keys disappear on their own. Rules stay in the sidecar's memory, indexed by endpoint, and are pulled from a small rules service every few seconds, so matching never leaves the pod. For a hot key I'd let the sidecar spend a local share and sync every hundred milliseconds, with the share set by how many sidecars actually see that key, so the error stays around one sync interval of the global rate. If a shard or the store fails, the sidecar fails open with a local bucket as a rough limit and alerts, and a bad rule is caught by validation plus a canary to a few sidecars before it reaches everyone.

    Common mistakes on this question

    • Starting with a central limiter service in the request path, which makes the limiter the single point of failure the requirements forbid.
    • Answering the bad-rule question with 'roll it back' — rollback is too slow when every sidecar has the rule within five seconds.

    Hints used: 1

Requirements, API and data, bottlenecks and how the design changed, each with your words.

Biggest gap: You had no concrete mechanism for rolling out a rule change safely: validation alone does not stop a bad rule reaching every sidecar within five seconds, so you need a staged canary with an automatic rollback on a deny-rate spike.

Requirements

  • Rules per key, user, IP, endpoint, runtimeNot confirmed
  • Under 5 ms, 1M decisions per secondCovered
    under five milliseconds added, a million checks a second, highly available, no single point of failureYour words, 3:09
  • Over-admission fine, blocking good traffic worseCovered
    is it worse to let a few extra requests through, or to block good traffic?Your words, 2:31
  • Limiter must not be a single point of failureCovered
    no single point of failure, and when in doubt we let traffic throughYour words, 3:09

Components

  • Token bucket, sliding window comparedCovered
    So a token bucket: per key we store the tokens left and the last refill timeYour words, 9:24
  • Sidecar per API instance, shared RedisCovered
    I'd rather make it a sidecar next to each API instance: no extra network hop, and the sidecar talks to Redis directlyYour words, 7:35
  • Redis sharded by key, atomic Lua scriptCovered
    In Redis I'd use a Lua script, which runs atomically on the shardYour words, 11:07
  • Local buckets synced periodically, approximateCovered
    the sidecar could keep a local bucket for that key and only sync with Redis every hundred millisecondsYour words, 18:54
  • Fail open, local fallback, rules pulledCovered
    it fails open for them, but keeps a local bucket as a rough limitYour words, 22:26
  • Bounded error from local-plus-sync designCovered
    the error is bounded by about one sync interval of the global rate, so about a hundred extra per intervalYour words, 20:16
  • Hot key switched to local modeCovered
    The sidecar can count its own calls per key; above some rate, say a few hundred a second, it switches that key to local modeYour words, 21:52
  • Rule rollout safety only half answeredPartly
    the rules service could validate a rule before accepting it, like reject a limit of zero on a whole endpointYour words, 28:45

    Validation catches obviously bad rules, but you still need a staged rollout: push a rule to a small set of sidecars first, watch the deny rate, and roll back automatically if it spikes.

API

Stated

Data model

Stated

Rule ID plus dimension value as the key, a small hash of tokens and last refill, and rules kept out of Redis entirely.

Bottlenecks

  • Central limiter as single point of failureraised by youHandled

    You caught this yourself and moved to a sidecar, which removes the shared hop.

  • Hot shard from one tenant's keyraised by AlexHandled

    Local buckets with periodic sync keep the hot key off the shard most of the time.

  • Redis cluster at ten times the loadraised by AlexHandled

    Adding shards plus leaning on local buckets is the right scaling answer for this design.

  • Metrics pipeline if every decision is loggedraised by youHandled

    Aggregating in the sidecar avoids a second hot path you would otherwise have created.

Trade-offs you discussed

  • Library vs sidecar

    A library would be even cheaper, no process boundary at allYour words, 8:10
  • Fixed window vs token bucket

    fixed windows let a client send double the limit around the window boundaryYour words, 9:24
  • Redis vs Memcached or a database

    Memcached is just as fast but has no scriptingYour words, 13:58
  • Async vs sync replication

    I wouldn't pay for synchronous replication here, that would add latency to every single checkYour words, 23:13
  • Per-region limits vs one global limit

    A truly global limit across regions would need cross-region calls in the request pathYour words, 30:29

How the design changed under follow-ups

  1. Alex asked at 7:32: “And what happens if the limiter is down?”

    Moved from a central limiter service to a sidecar per API instance

    I'd rather make it a sidecar next to each API instanceYour words, 7:35
  2. Alex asked at 9:22: “Which algorithm would the sidecar use?”

    Replaced the fixed window counter with a token bucket

    Not the fixed window I started withYour words, 9:24
  3. Alex asked at 18:47: “Let's stress it. One tenant suddenly sends most of the traffic on a single key. What happens?”

    Added a local bucket per hot key, synced every 100 ms

    the sidecar could keep a local bucket for that key and only sync with Redis every hundred millisecondsYour words, 18:54
  4. Alex asked at 30:21: “Different kind of change. A new region is added with its own traffic, and some API keys are used from both regions.”

    Split limits per region with a background job rebalancing shares

    I'd keep a Redis cluster per region and give each region its own share of each limitYour words, 30:29

Board and words

Decisions you explained

  • Limiter sidecar (per API pod) → Metrics + alerts (fail-open count)
    From the sidecar's side: slower replies and timeouts, which look like a shard failure, so it's the same fail-open path with the local bucket, and an alert.Your words, 24:28
  • Metrics + alerts (fail-open count)
    The important thing is that Redis never starts rejecting writes, because then every check errors out and we fail open everywhere at once.Your words, 23:40

Worth asking about, 1 more resolved

  • The Rules DB looks like a single instance on the main path; no replicas or redundancy are noted.
  • The Rules service looks like a single instance on the main path; no replicas or redundancy are noted.
  • The Limiter sidecar (per API pod) looks like a single instance on the main path; no replicas or redundancy are noted.

Practice next: Communication · 4 of 5

When an answer is thin, finish with one concrete option you would try next, then pause rather than trailing off.

Your whiteboard

The board as you left it, and what is on it.

What is on the board

  • Internal APIs (client)
  • Metrics + alerts (fail-open count) (monitoring)
  • Rules DB (database)
  • Redis cluster (token buckets) (cache, Redis, 2 replicas, 16 shards)
  • Redis cluster (region B) (cache, Redis, sharded)
  • Rules service (service)
  • Limiter sidecar (per API pod) (rate_limiter)
  • Rules snapshot (object_storage)

How data flows

  • Internal APIs → Limiter sidecar (per API pod): local call
  • Limiter sidecar (per API pod) → Redis cluster (token buckets): token bucket check
  • Limiter sidecar (per API pod) → Metrics + alerts (fail-open count): decisions, fail-open
  • Rules service → Rules DB
  • Limiter sidecar (per API pod) → Rules service: pull rules every 5 s
  • Rules service → Rules snapshot: publish
  • Redis cluster (token buckets) → Redis cluster (region B): share rebalance (async)

Your board had 8 components; 8 of the 8 parts you talked about were drawn.

Said, but not drawn

None.

On the board, not explained

  • Redis cluster (token buckets)
  • Limiter sidecar (per API pod)
  • Rules DB
  • Rules snapshot
  • Redis cluster (token buckets) → Redis cluster (region B)
  • Redis cluster (region B)
  • 2 replicas, sharded for Redis cluster (token buckets)
  • Sharded for Redis cluster (region B)

Open issues, your decisions and the evidence for each skill.

What the design still has to handle

  • Open

    The Rules DB looks like a single instance on the main path; no replicas or redundancy are noted.

    high severity

  • Open

    The Rules service looks like a single instance on the main path; no replicas or redundancy are noted.

    medium severity

  • Open

    The Limiter sidecar (per API pod) looks like a single instance on the main path; no replicas or redundancy are noted.

    low severity

  • Resolved

    The Redis cluster (token buckets) looks like a single instance on the main path; no replicas or redundancy are noted.

    low severity

Your design decisions

  • Redis cluster (token buckets)

    You did not say why. Saying the reason out loud is what interviewers score.

  • Limiter sidecar (per API pod)

    You did not say why. Saying the reason out loud is what interviewers score.

  • Limiter sidecar (per API pod) → Metrics + alerts (fail-open count)

    From the sidecar's side: slower replies and timeouts, which look like a shard failure, so it's the same fail-open path with the local bucket, and an alert.Your words, 24:28
  • Metrics + alerts (fail-open count)

    The important thing is that Redis never starts rejecting writes, because then every check errors out and we fail open everywhere at once.Your words, 23:40
  • Rules DB

    You did not say why. Saying the reason out loud is what interviewers score.

  • Rules snapshot

    You did not say why. Saying the reason out loud is what interviewers score.

  • Redis cluster (token buckets) → Redis cluster (region B)

    You did not say why. Saying the reason out loud is what interviewers score.

  • Redis cluster (region B)

    You did not say why. Saying the reason out loud is what interviewers score.

  • 2 replicas, sharded for Redis cluster (token buckets)

    You did not say why. Saying the reason out loud is what interviewers score.

  • Sharded for Redis cluster (region B)

    You did not say why. Saying the reason out loud is what interviewers score.

Evidence by skill

  • requirementsSome
    And the scale and the latency budget?Your words, 2:12
    So let me write down the requirements.Your words, 3:09
  • apiSome
    This term I'm doing a project with a small logistics startup, Fennick Freight, on their parcel tracking API.Your words, 0:23
    What are the limits keyed on: per user, per API key, per IP, something else?Your words, 1:44
  • data_modelSome
    What are the limits keyed on: per user, per API key, per IP, something else?Your words, 1:44
    Say ten million active keys at any time, across users, API keys and IPs; IPs are probably most of it.Your words, 3:53
  • scale_estimatesNot shown
  • high_level_designStrong

    On the board: 8 components, 7 connections; entry point drawn

    With a cache with a short expiry, the database load dropped by, I think, two thirds.Your words, 1:01
    Okay, so my first idea, the simplest thing: every API calls a central rate limiter service before handling a request, and the service keeps one counter per key in Redis.Your words, 5:27
  • deep_diveSome
    For a token bucket I need the tokens and a timestamp, so maybe sixteen bytes, plus, you know, the key itself and the hash overhead, call it, um, a hundredYour words, 3:53
    um, that's two network hops in sequence.Your words, 6:57
  • bottlenecks_failuresStrong

    On the board: redundancy on Redis cluster (token buckets); redundancy on Redis cluster (region B); resolved: spof

    That decides a lot of the failure behaviour.Your words, 2:31
    So let me write down the requirements.Your words, 3:09
  • tradeoffsStrong

    On the board: rationale for Limiter sidecar (per API pod) → Metrics + alerts (fail-open count); rationale for Metrics + alerts (fail-open count)

    Something like check, with the request's attributes: API key, user ID, IP, endpoint, and a cost, usually one, but an expensive call like a bulk export could cost more.Your words, 5:57
    The cost is a local call over localhost, which is well under a millisecond.Your words, 8:10
  • communicationStrong

    On the board: 18 of 18 board additions happened while a candidate turn was running

    So, mostly backend: Python, Postgres, a bit of Redis for caching tracking pages, because carriers hammer the same parcel IDs.Your words, 0:23
    And then we added a per-carrier limit, which, funnily enough, is sort of today's topic, I guess.Your words, 1:01

How the board grew

  1. 4:29Internal APIs · client

  2. 4:29Redis counters · cache

  3. 8:10local call · connection

  4. 8:10token bucket check · connection

  5. 8:10Limiter sidecar (per API pod) · rate limiter

  6. 24:28decisions, fail-open · connection

  7. 24:28Metrics + alerts (fail-open count) · monitoring

  8. 27:26rules->rdb · connection

  9. 27:26pull rules every 5 s · connection

  10. 27:26Rules DB · database

  11. 27:26Rules service · service

  12. 29:35publish · connection

  13. 29:35Rules snapshot · object storage

  14. 30:29share rebalance (async) · connection

  15. 30:29Redis cluster (region B) · cache

Coaching

The few changes that matter most, with a way to practice each one.

  1. 1Scale and failure handling

    Design config-change safety, not just rollback

    When asked about a bad rule reaching every sidecar in five seconds, your first answer was rollback plus manual review, and only after a follow-up did you reach validation and a canary.

    I guess we'd roll it back quickly? Maybe someone reviews rule changes before they go out.Your words, 28:22

    How to practice: In 20 minutes, write a numbered pipeline for a rule change: validation against recent traffic, shadow evaluation, canary to a few sidecars, an automatic rollback trigger on deny-rate, and who gets paged. Keep it to one page.

  2. 2Depth

    Bound the worst case yourself, with numbers

    You built local buckets well, but the first bound on over-admission was 'I'm not sure', and once Alex gave you numbers you produced a clean bound in the same turn. Getting there without the prompt makes your design sound owned rather than sketched.

    I'm not sure of the exact bound, I'd have to work it out.Your words, 19:39

    How to practice: Do it in 15 minutes without looking back at the transcript.

  3. 3Depth

    Turn an unknown into a stated default

    On resharding you said you didn't know the details and stopped, even though you already knew about MOVED replies. A short 'here is what I'd assume and how I'd confirm it' keeps the design moving.

    What happens to in-flight requests during resharding, honestly, I don't know the details.Your words, 14:48

    How to practice: List three things you don't know about Redis Cluster (resharding, failover, eviction under pressure). For each, write one sentence of safe assumption and one sentence of how you'd verify it. 15 minutes.

  • I guess we'd roll it back quickly? Maybe someone reviews rule changes before they go out.Your words, 28:22

    Before a rule reaches the sidecars I'd validate it against recent traffic and send it to a few sidecars first, watching the deny rate. Rollback is the fallback, not the first defence.

    You lead with prevention in the same breath as rollback, so the answer sounds designed rather than improvised.

  • I'm not sure of the exact bound, I'd have to work it out.Your words, 19:39

    I haven't computed the exact bound yet, but the shape is clear: each sidecar can overspend its share for one sync interval, so the error scales with the interval. Let me put numbers on it.

    You name the mechanism first, then commit to computing, which is what you then actually did.

  1. Day 1Write the safe rule-rollout pipeline, then say it aloud in 90 seconds20 min
  2. Day 2Redo the 50-sidecar over-admission bound on paper, stating assumptions first20 min
  3. Day 3Rehearse a 60-second answer to 'what breaks first at 10x load'10 min
  4. Day 4Rest day: no practice5 min
  5. Day 5Redraw the limiter end to end on one page from memory20 min
  6. Day 6List three Redis unknowns with a safe assumption and a check for each15 min
  7. Day 7Answer the two probe questions again out loud, timed20 min

Pace, pauses, fillers and pitch, measured from your voice. Signals, not judgments.

Moments worth a look

  • You hesitated and used hedging phrases

    When asked “What if someone pushes a bad rule that blocks all traffic?” (Q1), you hesitated and used hedging phrases: you took 7.2 s to start (your usual 3.5 s); a 3.3 s pause mid-answer; your pitch went up at the end of 2 of 2 phrases; 2 hedges such as 'I guess'.

    • Time to start 7.2 s vs your usual 3.5 s
    • Longest pause 3.3 s
    • Your pitch went up at phrase ends 2 of 2
    • Hedging 2 (“I guess”, “Maybe”)
    I guess we'd roll it back quickly?Your words, 28:22

    Try: Start with a one-line headline ('My approach is X because Y') and think aloud from there instead of in silence.

  • You used several hedging phrases

    When asked “How much state is that?” (Q1), you used several hedging phrases: 8 fillers per minute: 'um' (2), 'uh' (1), 'you know' (1); you sped up to 180 words per minute (your usual 140); 2 hedges such as 'probably'.

    • Fillers per minute 8.2 (“um”, “uh”, “you know”)
    • Words per minute 180 vs your usual 140
    • Hedging 2 (“probably”, “maybe”)
    Say ten million active keys at any time, across users, API keys and IPs; IPs are probably mostYour words, 3:53

    Try: Say the part you are sure of plainly, then name the one part you are not: 'X holds; I'm not certain about Y, I'd check it by Z.'

  • You hesitated

    When asked “Next. Now the Redis shard holding a set of keys goes down....” (Q1), you hesitated: you took 7.8 s to start (your usual 3.5 s); a 3.0 s pause mid-answer.

    • Time to start 7.8 s vs your usual 3.5 s
    • Longest pause 3.0 s
    Then the sidecar can't get a decision for those keys.Your words, 22:26

    Try: Start with a one-line headline ('My approach is X because Y') and think aloud from there instead of in silence.

Voice signals

Measured in 49 of 52 answers with audio.

  • Your pitch went up at 20 of 329 measured phrase endings.

3 of 52 answers did not have enough clear audio to measure (background noise, too little speech).

When a question catches you out, say what kind of answer is coming before the detail, rather than opening with a hedge; that turn had your highest hedge count.

On quantitative follow-ups your reply started later than elsewhere; naming the variables first and then computing out loud shortens that gap.

Pace, pauses and fillers

Measured across 52 answers: average speaking rate 138 wpm, 0.8 fillers per speaking minute, average time to start answering 3.8 s, longest mid-answer pause 3.3 s.

Pace
138words/min
Fillers
0.8per min
Longest pause
3.3s
Time to first word
3.8s
Average before you started answering
Speaking time
83%
Share of answer time spent speaking
Hedges
21
Phrases like "I think" or "maybe"

Answer by answer

Pause Strong Hesitation Rushing Trailing off Rambling

The answers with a marked moment. Notes on single answers use typical speaking ranges.

Question 1

  • 0s19s
    Length
    19s
    Pace
    128
    To first word
    7.2s
    Fillers
    1
    Hedges
    2
    Pauses
    1
    • Hesitation (0s to 17s): Start came after 7.2 s, with 1 filler and 2 hedge phrases in the answer.
    • Trailing off (15s to 19s): Voice level over the last 4 s was about 8 dB lower than earlier in the answer.
  • 0s46s
    Length
    46s
    Pace
    127
    To first word
    7.4s
    Fillers
    1
    Hedges
    1
    Pauses
    0
    • Trailing off (35s to 46s): Voice level over the last 11 s was about 8 dB lower than earlier in the answer.
    • Strong (0s to 46s): Steady rate (127 wpm), no pauses over 1.5 s and few fillers.
  • 29 steady stretches noted.
  • 1 hesitation episode noted.
  • 2 trailing-off endings noted.

What Alex wrote down while listening.

  1. ClaimQuestion 12:12

    Asked for scale, latency budget, failure preference and regions before drawing.

  2. ClaimQuestion 15:57

    Defined check() with cost and retry_after for a 429.

  3. ClaimQuestion 17:35

    Spotted the central service as a single point of failure after Alex asked; moved to a sidecar.

  4. ClaimQuestion 111:41

    Atomic Lua refill-and-take using Redis server time; TTL on idle keys.

  5. ClaimQuestion 113:07

    Data model: key = rule id + dimension value, hash of tokens and last refill.

  6. GapQuestion 114:48

    Did not know how resharding affects in-flight requests; said so.

  7. ClaimQuestion 116:47

    Rules indexed by endpoint with a prefix tree; atomic swap on update.

  8. GapQuestion 119:39

    Could not bound the over-admission of local buckets at first.

  9. ClaimQuestion 126:03

    10k-IP burst: per-key and per-endpoint limits; priority for known-good keys.

  10. GapQuestion 128:22

    Bad rule push: first only 'roll it back' and manual review.

  11. ClaimQuestion 128:45

    After a probe: validation and a canary rollout, not worked out in detail.

  12. ClaimQuestion 132:13

    Named rule rollout and multi-rule token waste as weak points.

104 turns, word for word.

  1. Alex0:02

    Hi, I'm Alex. In this round we'll design a system together, about forty-five minutes. The whiteboard is on the right; draw as you go, and talk me through your decisions. There's no single right answer. We'll keep a few minutes at the end for your questions. Before that, what are you working on these days?

  2. You0:23

    Hi. I'm in my final year, computer science. This term I'm doing a project with a small logistics startup, Fennick Freight, on their parcel tracking API. So, mostly backend: Python, Postgres, a bit of Redis for caching tracking pages, because carriers hammer the same parcel IDs. It's been fun, and a bit scary, it's the first time my code has real users. Nothing at the kind of scale you see in these interviews, though.

  3. Alex0:58

    Okay. What did the Redis cache fix for you?

  4. You1:01

    Response times, mostly. One carrier was polling every parcel every thirty seconds, and the tracking page did four joins. With a cache with a short expiry, the database load dropped by, I think, two thirds. And then we added a per-carrier limit, which, funnily enough, is sort of today's topic, I guess. It was just a counter with an expiry, nothing clever.

  5. Alex1:32

    Okay. Here's the problem. Design a distributed rate limiting service used by hundreds of internal APIs to protect themselves from overload and abuse. Take it wherever you want; start when you're ready.

  6. You1:44

    Okay. Before I draw anything, a few questions. What are the limits keyed on: per user, per API key, per IP, something else? And do the rules change often, or are they basically static config?

  7. Alex2:02

    Per API key, per user, per IP and per endpoint, and combinations of those. Teams want to change rules without a deploy, sometimes several times a day.

  8. You2:12

    And the scale and the latency budget? Since it sits in the request path of every call, it can't be slow.

  9. Alex2:23

    About a million decisions per second at peak, across all the APIs. A decision should add under five milliseconds.

  10. You2:31

    Okay. And when we're unsure, is it worse to let a few extra requests through, or to block good traffic? That decides a lot of the failure behaviour.

  11. Alex2:45

    Wrongly blocking good traffic is worse. Occasional over-admission is fine. And the limiter must not become a single point of failure.

  12. You2:53

    One more: is it one region, or several? And are limits usually per second, per minute?

  13. Alex3:03

    One region for now. Limits are mostly per second and per minute, some per day.

  14. You3:09

    Got it. So let me write down the requirements. Functional: for each request, check it against all the rules that match it, and return allow or deny. Teams can create and edit rules at runtime. Non-functional: under five milliseconds added, a million checks a second, highly available, no single point of failure, and when in doubt we let traffic through. Accuracy can be approximate, a few percent over the limit is fine. And I'd like to be able to see what's being denied.

  15. Alex3:47

    How much state is that?

  16. You3:53

    Um, let me estimate. Say ten million active keys at any time, across users, API keys and IPs; IPs are probably most of it. For a token bucket I need the tokens and a timestamp, so maybe sixteen bytes, plus, you know, the key itself and the hash overhead, call it, um, a hundred bytes per entry. That's, uh, ten million times a hundred, a gigabyte. So it fits in memory, on a few machines with headroom. Even ten times more keys would be fine across a cluster.

  17. Alex4:26

    And the request rate on that store?

  18. You4:29

    If every decision touches the store once, a million operations a second. More, if a request matches several rules. A single Redis node does maybe a hundred thousand simple operations a second, a bit fewer with scripts, so roughly ten shards, more with headroom. Sixteen, say. That's a sizable cluster but not crazy.

  19. You5:27

    Okay, so my first idea, the simplest thing: every API calls a central rate limiter service before handling a request, and the service keeps one counter per key in Redis. INCR with an expiry, so a fixed window per minute, like what I did at Fennick. If the counter is over the limit, deny.

  20. Alex5:54

    What does the API look like?

  21. You5:57

    Something like check, with the request's attributes: API key, user ID, IP, endpoint, and a cost, usually one, but an expensive call like a bulk export could cost more. It returns allowed or not, the remaining tokens, and a retry-after in milliseconds, so the API can send a proper four-twenty-nine with a Retry-After header. The service works out which rules match; the caller doesn't need to know the rules.

  22. Alex6:53

    Walk me through one request. Where do the five milliseconds go?

  23. You6:57

    The API receives a request and calls the limiter, the limiter calls Redis, and back... um, that's two network hops in sequence. Inside one data center that's maybe half a millisecond to a millisecond each, so it fits, with room for some queueing. But, hmm, at a million a second the limiter service becomes the thing everything depends on.

  24. Alex7:32

    And what happens if the limiter is down?

  25. You7:35

    Then every API is either blocked, or if they time out and allow, nothing is protected. So it's a single point of failure, which you said it must not be. I'd rather make it a sidecar next to each API instance: no extra network hop, and the sidecar talks to Redis directly. If one sidecar dies, it only affects its own pod.

  26. Alex8:07

    Why a sidecar and not a library?

  27. You8:10

    A library would be even cheaper, no process boundary at all. But with hundreds of APIs they're probably in different languages, so I'd have to write and upgrade the client in each one, and upgrades would depend on every team redeploying. A sidecar is one implementation, and we can roll it out ourselves. The cost is a local call over localhost, which is well under a millisecond.

  28. Alex9:22

    Which algorithm would the sidecar use?

  29. You9:24

    Not the fixed window I started with: fixed windows let a client send double the limit around the window boundary, a full minute's worth at the end of one window and again at the start of the next. So a token bucket: per key we store the tokens left and the last refill time, and refill on every read based on the time that passed. It allows short bursts up to the bucket size, which fits 'don't block good traffic'. A sliding window log is more exact, but it stores every timestamp, which is too much memory at a million a second.

  30. Alex10:11

    Sliding window counter?

  31. You10:13

    Um, that's the one with two fixed windows and a weighted sum? The current window's count plus the previous window's count times the fraction of it that's still inside the sliding window. That's cheap, two counters per key, and close enough in practice. It's honestly also a fine choice. I'd still go with the bucket because teams think in 'a hundred a second with bursts of two hundred', and that maps directly to the refill rate and the capacity.

  32. Alex11:01

    How do you make the check-and-decrement safe when many sidecars hit the same key?

  33. You11:07

    If two sidecars read the bucket, both see one token, and both write back zero, we admitted two. So the read, refill and decrement have to be one atomic step. In Redis I'd use a Lua script, which runs atomically on the shard. And Redis is sharded by a hash of the key, so one key always lives on one shard, so there's no cross-shard coordination for a single key.

  34. Alex11:39

    Walk me through the script.

  35. You11:41

    It gets the key, so a hash with tokens and last refill. Takes the current time... um, actually, time is a problem, the sidecars' clocks differ, and if one is ahead it refills too much. I'd use the Redis server time inside the script, so one clock per key. Then tokens becomes the min of the capacity and tokens plus elapsed time times the rate. If tokens is at least the cost, subtract and allow, else deny and compute the retry-after from the deficit. Write back, set an expiry so idle keys disappear on their own, and return the result.

  36. Alex13:03

    What exactly is a key? Spell out the data model.

  37. You13:07

    A rule has an ID, a scope, like per user on endpoint X, a rate and a capacity. The bucket key in Redis is the rule ID plus the value of the dimension, so something like rl, colon, rule 42, colon, user 1234. The value is a small hash with two fields, tokens and last refill. The rule itself, the rate and capacity, lives in the sidecar's memory and gets passed into the script as arguments, so Redis doesn't need to know about rules at all. That also means a rule change applies on the next check without touching Redis.

  38. Alex13:56

    Why Redis and not something else?

  39. You13:58

    Mostly because it's in memory, sub-millisecond, and it can run the check atomically with a script. Memcached is just as fast but has no scripting, so I'd need compare-and-swap loops, which get ugly on a hot key. A database like Postgres or DynamoDB is too slow and too expensive per operation at a million a second. A custom in-memory service would work too, but then we'd be building and running our own Redis. I'd only do that later if Redis became the limit.

  40. Alex14:44

    How does a sidecar know which shard holds a key?

  41. You14:48

    With Redis Cluster, the key is hashed into one of, I think, sixteen thousand three hundred and eighty-four slots, with CRC16, and each node owns a range of slots. The client caches the slot map and goes straight to the right node; if the map is stale, it gets a MOVED reply and refreshes. What happens to in-flight requests during resharding, honestly, I don't know the details. I'd have to read up on that.

  42. Alex15:31

    One request matches four rules. How many round trips?

  43. You15:34

    Four keys, user, API key, IP and endpoint, possibly on four different shards. If I do them one by one that's four hops in sequence, which eats the budget. I'd send them in parallel, so latency is the slowest one, not the sum. Um, but then one could allow and another deny, and the allowed ones have already taken a token.

  44. Alex16:06

    Does that matter?

  45. You16:08

    A little: a denied request still costs tokens on the other keys, so a user who's hitting the endpoint limit also burns their own user budget. Given we prefer over-admission, I'd mostly accept it, or refund the tokens asynchronously after a deny. I wouldn't do a distributed transaction for this, it's far too slow for five milliseconds.

  46. Alex16:42

    How does the sidecar find the rules that match a request quickly?

  47. You16:47

    There could be thousands of rules, so not a linear scan per request. I'd index them by endpoint first, with an exact-match map plus a small prefix tree for path patterns like slash orders slash star. Then for the few rules on that endpoint, check which dimensions apply: does it have an API key, which user. Rules that apply to every endpoint, like a global per-IP rule, sit in their own list. That should be microseconds. The index gets rebuilt whenever new rules arrive, and swapped in atomically, so a request never sees half the rules.

  48. Alex17:34

    And the API, when it gets a deny?

  49. You17:37

    Returns a four-twenty-nine with Retry-After. Well-behaved clients back off with jitter. Internal clients we control should use a shared client library that respects it, otherwise they retry immediately and make the overload worse.

  50. Alex17:56

    Put numbers on the request path now, end to end.

  51. You18:00

    Okay. The API to the sidecar over localhost, maybe a tenth of a millisecond. Rule matching, microseconds. Then the Redis calls in parallel, inside one data center, about half a millisecond at the median, but the p99 could be two or three milliseconds when a shard is busy. So the median is under a millisecond and the tail is what matters. That's why I want the hard timeout at about two milliseconds, and after that the sidecar decides locally. So we stay under five even in the bad case.

  52. Alex18:47

    Let's stress it. One tenant suddenly sends most of the traffic on a single key. What happens?

  53. You18:54

    That key's shard gets hot. Every check for that key goes to one Redis node, and a single node tops out at maybe a hundred thousand a second, and it slows down every other key on that shard. Um, the sidecar could keep a local bucket for that key and only sync with Redis every hundred milliseconds, so it spends a share of the limit locally. It over-admits a bit between syncs, which we said is acceptable.

  54. Alex19:37

    How much can it over-admit?

  55. You19:39

    Roughly... if every sidecar can spend its local share for one sync interval before it hears from Redis, the worst case is about one interval of extra traffic per sidecar. I'm not sure of the exact bound, I'd have to work it out.

  56. Alex20:10

    Try it with numbers. A limit of a thousand a second, fifty sidecars.

  57. You20:16

    Okay. If each sidecar gets an equal share, that's twenty a second each. Hmm, but the traffic isn't spread equally, so the share is wrong for most of them... If instead each sidecar can spend up to the full rate locally for a hundred milliseconds, that's a hundred requests each before syncing, times fifty is five thousand in a tenth of a second. That's way over. So the local share has to be smaller, like the limit divided by the number of sidecars that are actually seeing that key, which Redis can count. Then the error is bounded by about one sync interval of the global rate, so about a hundred extra per interval.

  58. Alex21:49

    When does a key switch to the local mode?

  59. You21:52

    The sidecar can count its own calls per key; above some rate, say a few hundred a second, it switches that key to local mode. Or Redis tells it, since the script can see how often a key is hit. I'd start with the sidecar deciding, it's simpler.

  60. Alex22:19

    Next. Now the Redis shard holding a set of keys goes down. Say it's peak traffic.

  61. You22:26

    Then the sidecar can't get a decision for those keys. Since blocking good traffic is worse, it fails open for them, but keeps a local bucket as a rough limit, so one sidecar can't let through unlimited traffic. And we alert on it. Each shard has a replica, so failover should take seconds, not minutes. The sidecar needs a tight timeout on Redis, like two milliseconds, so a slow shard doesn't eat the whole budget.

  62. Alex23:11

    What does the replica have?

  63. You23:13

    Redis replication is asynchronous, so it's slightly behind. After failover some buckets are a bit fuller than they should be, so we over-admit for a moment. Again, acceptable. I wouldn't pay for synchronous replication here, that would add latency to every single check.

  64. Alex23:37

    And if Redis runs out of memory?

  65. You23:40

    Every bucket has a TTL, so idle keys go away by themselves. If it still fills up, I'd set the eviction policy to evict keys with a TTL, least recently used first. An evicted bucket comes back full, so the worst case is over-admission for that key, which fits the policy. The important thing is that Redis never starts rejecting writes, because then every check errors out and we fail open everywhere at once. Um, and with the cluster bus, when a node is under memory pressure, the gossip—

  66. Alex24:24

    Let's stay at the level of what the sidecar sees.

  67. You24:28

    Sure. From the sidecar's side: slower replies and timeouts, which look like a shard failure, so it's the same fail-open path with the local bucket, and an alert.

  68. Alex25:07

    Some limits are per day. Does the token bucket still work?

  69. You25:11

    Mm, it works mathematically, the refill rate is just tiny, a thousand a day is about one every ninety seconds. But it means the TTL has to be longer than a day, and teams usually mean 'resets at midnight', not 'refills smoothly'. For daily quotas I'd use a plain counter keyed by the date, with an expiry of a couple of days. Those are more like billing quotas than overload protection anyway, they don't need to be checked on the hot path at the same precision.

  70. Alex26:00

    A client sends a burst from ten thousand IPs.

  71. You26:03

    So per-IP limits don't help, each IP is under its limit. That's where the per-endpoint and per-API-key limits matter: if it's all one API key, its bucket runs dry and we deny. If there's no key, like a public login endpoint, the per-endpoint global limit protects the service, but then good users get blocked too.

  72. Alex26:34

    Which you said is the worse outcome.

  73. You26:37

    Right. Um. I could... prioritise. Keep a reserved part of the capacity for keys and users we've seen before with good history, and shed the unknown traffic first. Grouping IPs by subnet would help too, ten thousand IPs are often a few ranges. But honestly, at that point it's more of a DDoS problem, and I'd expect something at the edge to absorb most of it before it ever reaches the internal APIs.

  74. Alex27:22

    And the rules. How do they get to the sidecars?

  75. You27:26

    A rules service with its own database, Postgres is fine, it's tiny and changes rarely compared to the traffic. The sidecars pull the rules every few seconds and cache them in memory, so matching a request to rules never leaves the pod.

  76. Alex28:18

    What if someone pushes a bad rule that blocks all traffic?

  77. You28:22

    Oh. Hmm. I guess we'd roll it back quickly? Maybe someone reviews rule changes before they go out.

  78. Alex28:42

    Within five seconds every sidecar has it.

  79. You28:45

    Right, so a review doesn't save us when the mistake gets through, and rollback is too late. Then... the rules service could validate a rule before accepting it, like reject a limit of zero on a whole endpoint, or a limit far below the current traffic. And, um, maybe roll it out to a few sidecars first and watch the deny rate before it goes everywhere. I haven't thought about how you'd do that cleanly, it's a bit vague.

  80. Alex29:32

    And if the rules service is down?

  81. You29:35

    The sidecars keep the last rules they have, they're cached, so nothing changes for running pods. New sidecars starting up would need a copy, so I'd also publish a snapshot to object storage that they can read at startup, and the rules service writes it on every change.

  82. Alex30:21

    Different kind of change. A new region is added with its own traffic, and some API keys are used from both regions.

  83. You30:29

    Um, I'd keep a Redis cluster per region and give each region its own share of each limit. A truly global limit across regions would need cross-region calls in the request path, which is tens of milliseconds and kills the five-millisecond budget. So limits become per region, maybe rebalanced every few seconds by a background job that looks at where the traffic actually is. The rules themselves can be replicated, they're small.

  84. Alex31:34

    What breaks first at ten times the load?

  85. You31:37

    The Redis cluster, ten million operations a second. I'd add shards, that part scales horizontally, and lean more on local buckets for keys with high traffic, so most decisions never leave the pod. The rules pull is tiny, it scales fine. And the metrics pipeline, actually, if we log every decision; I'd aggregate in the sidecar and only ship counts.

  86. Alex32:10

    What are the weakest parts of your design?

  87. You32:13

    Um, the rule rollout, definitely, I only half have an answer there. And the multi-rule check, the tokens taken by a request that's denied elsewhere. And I never defined how the over-admission is measured, I'd want a metric for that, like admitted versus limit per key.

  88. Alex32:39

    How would you monitor it?

  89. You32:41

    Per sidecar: decision latency, the Redis error and timeout rate, fail-open count, and deny rate per rule. An alert when a single rule's deny rate jumps, which also catches the bad-rule case. And a dashboard per API team, so they can see their own limits.

  90. Alex33:04

    How would you test this before it's in front of real traffic?

  91. You33:09

    Load test with replayed production traffic, so the key distribution is realistic, including the hot keys, because synthetic uniform traffic would hide the hot-shard problem. Then failure tests in a staging cluster: kill a Redis primary at peak and check that the fail-open rate spikes and recovers, and that latency stays under five milliseconds. And a test for the bad-rule case, push a zero rule and make sure validation rejects it.

  92. Alex33:46

    How would you roll the sidecar out?

  93. You33:49

    Shadow mode first: it makes decisions and logs them, but never denies. Compare with what the APIs see and tune the rules, then turn on enforcement per API, starting with the ones that want it most, probably the ones that have been overloaded before.

  94. Alex34:12

    Version two?

  95. You34:14

    A self-service UI for rules with a dry-run button that shows how much traffic a rule would have denied yesterday. I think that also helps the bad-rule problem, people would see the zero-limit mistake before saving it.

  96. Alex34:35

    Okay. We have a few minutes left. Any questions for me?

  97. You34:40

    Yes. How do teams usually split work like this, like who would own something like a rate limiter?

  98. Alex34:50

    Usually a platform or infrastructure team owns shared pieces like this, with the API teams as its customers. The hiring team can tell you how it works for this particular role.

  99. You35:02

    Okay. And how much of the job is design versus writing code, for a new grad?

  100. Alex35:11

    Mostly code at first, with design reviews you'd sit in on and contribute to over time. The specifics vary by team.

  101. You35:19

    And is there anything about this kind of service that usually surprises people when they actually build it?

  102. Alex35:30

    Usually the operational side: rule changes, debugging why a request was denied, and who gets paged when the limiter itself misbehaves. The algorithm tends to be the easy part.

  103. You35:41

    That makes sense. Thanks, that's all from me.

  104. Alex35:47

    Thanks, that's time. You'll get your report in a few minutes.

Mockich is a practice tool. Scores and delivery signals are guidance for your preparation, not a prediction of a real hiring decision.

Start another interview
Sample report · Mockich