cswork / libraryOpen practice workspace

Interview guides · Discord

Discord Software Engineer Interview Guide

Discord Software Engineer interview guide: socket chat servers in your own IDE, etcd leader election, matchmaking design, incident debugging, 7-day plan.

Interviewing at Discord

  • Practice bank: 11+ questions
  • Rounds: 3
  • Typical prep: 2–4 weeks
  • Interview reports: 10

Interview rounds

RoundQuestions
HR Screen3
Technical Screen2
Onsite6

How hard is the Discord Software Engineer interview?

DifficultyShareQuestions
Easy9%1
Medium55%6
Hard36%4

What Discord actually tests for

TopicShareQuestions
Software Engineering Fundamentals36%4
System Design36%4
Coding & Algorithms18%2
Behavioral & Leadership9%1

The 60-second brief

Discord runs real-time voice, video and text communication for online communities. The Software Engineer role works on whichever part of that platform your team owns. Reported team areas include Core Infrastructure, Persistence, Safety Processing, Monetization (Ads and Quests), Client Platform and Experimentation. The reported stack includes Python, Rust, Go, TypeScript, React, React Native, ScyllaDB, PostgreSQL, Kafka and etcd. The questions candidates report are close to that work: socket servers, distributed coordination, message storage and live troubleshooting, not puzzle-style problems.

Discord candidates report 4 rounds · ≈ 3-5 weeks. The stages below are what candidates describe, not a published process.

What a Software Engineer does at Discord

The reported stack is Python, Rust, Go, TypeScript, React, React Native, ScyllaDB, PostgreSQL, Kafka and etcd, and the reported stack also includes Redis, ElasticSearch, Docker and Kubernetes. You do not need all of them. For live coding, use the language you write fastest in. Be ready to compare a relational store with a wide-column store for high-write message data. Know a coordination service like etcd well enough to explain leases, TTLs and failover.

Candidates say the interview favors practical engineering over abstract puzzles. Candidates report coding in their own local IDE over screen share. Reported prompts include a multi-client TCP or WebSocket chat server, an etcd-based leader-follower cluster, a matchmaking design, sessionizing event logs, and walking through a CPU spike or dropped connections. The question bank still lists algorithmic items such as KMP string matching, DFS backtracking and trie-based completions. Keep some data-structure practice in your week, but put the hands-on work first.

The Discord interview process

Discord candidates report 4 rounds · ≈ 3-5 weeks. The stages below are what candidates describe, not a published process.

Recruiter Screen

The recruiter screen is reported as a conversation about your background and fit for the role. Use it to get the facts that shape your preparation. Ask which team or domain the role sits in; reported team areas range from Core Infrastructure and Persistence to Safety Processing, Monetization and Client Platform. Ask whether the technical screen is a hands-on build in your own IDE, which languages are accepted, and what tools you may use in coding rounds. State your hard constraints (start date, location, work authorisation, competing timelines) now so none of them comes up late.

What it evaluates

  • Whether your background fits the team area the role is hiring for
  • Whether you can summarise your recent work in a few sentences and name the scope you owned
  • Whether your constraints and timeline fit the process

How to prepare

  • Write a short summary of your last two roles that names the systems you owned, the language you used and one concrete result
  • Ask which team the role belongs to and adjust your week: infrastructure teams point toward sockets, etcd and storage, and client teams toward React components and real-time state
  • Ask whether coding happens in your local IDE, which languages are fine, and whether documentation or AI assistants are allowed in each round

Hiring Manager Call

Candidates describe this as a call with the hiring manager to align on expectations and on your qualifications. Treat it as a shorter version of the project retrospective, since the conversation is about whether your experience maps to the team's work. Bring one or two projects you can explain as decisions: why that datastore, why that concurrency model, what you traded off and what failed. Connect them to the team area the recruiter named. End with questions about what the team is building and how on-call works.

What it evaluates

  • Whether your past work maps to the team's domain, such as infrastructure, safety processing or client platform
  • Whether you can explain decisions you owned, the alternatives you rejected and why
  • Whether you can explain trade-offs clearly to someone who did not work on the project

How to prepare

  • For each of two projects, write the decision you made, the option you rejected and the constraint that settled it
  • Prepare one example of a technical disagreement and how you resolved it with evidence such as a benchmark or a prototype
  • Prepare three questions about the team's current systems, its on-call load and the work planned for your first months

Technical Screen

Candidates describe a 60-to-75-minute practical screen focused on hands-on live implementation in your own IDE over screen share. It often means building a working network application such as a multi-client socket chat server. The prompt usually grows: first a working broadcast, then follow-ups such as nicknames, listing online users, clean disconnects and system notifications. Practise getting a minimal version running and demonstrable with telnet or netcat, then extending it without breaking it. Before you write the code, explain your concurrency choice: threads with a lock, or an asyncio event loop.

What it evaluates

  • Whether you can start a TCP or WebSocket server from the standard library and demonstrate it with several clients
  • Whether concurrent clients are handled safely: shared client state, broadcasts that skip the sender, and disconnects that do not crash the server
  • Whether you get a working minimal version first and extend it cleanly as requirements are added
  • Whether you explain your approach and debug errors out loud as you go

How to prepare

  • Build the multi-client chat server twice from blank, once with threads and once with asyncio, and test each with three telnet sessions
  • Add the reported follow-ups one at a time: a nickname on connect, a command listing online users, join and leave notices, and handling a client killed mid-message
  • Frame input on newlines so you handle a message split across two reads, and two messages arriving in one read
  • Rehearse the setup: project open, run command ready, loopback ports allowed through the firewall

Virtual Onsite Loop

Candidates report the onsite as four to five virtual interview blocks covering live coding, system design, incident troubleshooting, a project retrospective and values alignment. Team-specific loops can change individual topics. Prepare each block as a separate skill. For coding, expect the same practical style as the screen. For the design questions in this guide (matchmaking on skill, latency and wait time; message storage at very large scale on ScyllaDB or PostgreSQL; an etcd leader-follower cluster), state the requirement before you choose a component. For troubleshooting, practise a hypothesis-driven walk-through of a CPU spike or of dropped connections. For the retrospective, prepare one project in depth.

What it evaluates

  • Whether design answers start from requirements and name trade-offs, such as a partition key for message data and the queries it makes expensive
  • Whether your debugging walk-through asks for evidence, separates mitigation from root cause and names what each check rules out
  • Whether a project retrospective holds up under follow-ups about missteps, edge cases and friction with other teams
  • Whether live coding keeps the same working-first approach as the technical screen

How to prepare

  • Talk through the matchmaking design aloud: queues per region and skill tier, how the allowed skill gap widens with wait time, and what happens when a match forms but a player drops
  • Model message storage for a chat app: partition by channel and time bucket, order by a time-sortable message id, and say what that design makes hard, such as cross-channel search or hot channels
  • Write a one-page incident checklist (what changed, which metrics, blast radius, mitigation, root cause, follow-up) and run the CPU-spike prompt against it
  • Prepare the retrospective and values stories listed in the behavioral section

Questions Discord candidates report

System design and architecture

At any point in a design you should be able to name the one resource that gives out first and what it costs to move it. Most follow-ups push on exactly that: traffic rises tenfold, a region is lost, a stricter consistency requirement appears. Answers hold up when the assumptions were said out loud early, because the design can then be re-derived from them rather than defended. The weak move under pressure is to add a component. The strong one is to say which part absorbs the change, which part has to be rebuilt, and roughly what the rebuild costs.

  • Design a high-throughput matchmaking system for a multiplayer online game that pairs players based on skill tiers, latency constraints, and wait time.
  • Implement a leader-follower distributed cluster using etcd key-value storage where nodes perform leader election, periodic TTL updates, and automatic failover detection.

Debugging and production issues

Experienced debuggers do not search the space evenly; they read the shape of the symptom first, because the shape narrows the class before any tool is opened. A sharp edge at a specific timestamp points at something that changed. A slow climb that resets on restart points at something unbounded. A fixed hour points at scheduled work or an expiry. A fraction of requests failing identically points at one partition — a node, a shard, a tenant, a stale cache entry on one replica. Then pick two candidates and name the single observation that separates them. Listing ten possible causes is not a hypothesis.

  • Evaluate C++ memory alignment padding issues or ABI binary interface compatibility challenges when maintaining client library runtimes.
  • Analyze a system failure involving broken socket pipes, network drops, or thread deadlocks during high concurrent connection bursts.

Behavioural and engineering judgement

Ownership questions here usually point at something with no owner and no deadline: an alert people mute, a suite everyone reruns until it passes, a service whose on-call is a rota of volunteers. Nothing forces action, so what you did says how you spend discretionary time. The strongest answers are not the ones where you fixed everything. They are the ones where you sized the problem first, chose between fixing it, containing it and writing it down for whoever comes next, told someone who could have overruled you, and can still say why the other two options were worse.

  • Tell me about a time when you were building a feature and had to make critical technical trade-offs under tight deadline pressure.
  • Describe a situation where you had a significant technical disagreement with your manager or team members, and explain how you arrived at a resolution.

Other topics

Nothing constrains the range here, so expect at least one question at the edge of what you know. Answer at that edge deliberately: give the adjacent thing you do know, say plainly where your knowledge stops, and reason forward from the part you can defend. Calibration is itself information about you, and it is cheap to check, since one follow-up asking why will empty out an answer with nothing under it. Everything you have claimed as experience is fair material, so the item you last touched three years ago is worth an hour of re-reading rather than a hopeful skip.

  • Implement a interactive feature end-to-end, defining both state structures and backend endpoints for real-time state synchronization.
  • Design and code a interactive Snake game or real-time game board handling state ticks, keyboard input events, dynamic collision boundaries, and thread-safe state rendering.

Practice exercises with worked solutions

The prompts above tell you what gets asked.

Make the like write idempotent and the counter conditional

engagement holds actor_id, content_id, action_kind ('like','save','repost','hide','report','not_interested'), idempotency_key UUID, created_at_utc, undone_at_utc and origin_surface, with UNIQUE (actor_id, content_id, action_kind). A phone on a flaky connection sends the same like three times, and the user later unlikes and re-likes. Write the exact statements for like and for unlike so the table ends in the right state and the item's like counter moves exactly once per real state change. State how the handler distinguishes an insert from an existing row, and what a duplicate-key violation means here.

How a strong candidate works it

  • Do not read first. SELECT-then-INSERT races: both sessions see no row, both insert, and one fails on the unique index after the effect the user asked for already succeeded. The constraint is the mechanism that provides the property, not a backstop behind an application check.
  • Like: INSERT INTO engagement (...) VALUES (...) ON CONFLICT (actor_id, content_id, action_kind) DO UPDATE SET undone_at_utc = NULL, idempotency_key = EXCLUDED.idempotency_key WHERE engagement.undone_at_utc IS NOT NULL RETURNING actor_id. A fresh like inserts, a re-like after an unlike updates and returns a row, and a duplicate of a live like matches nothing in the DO UPDATE predicate and returns zero rows.
  • Gate the counter on that rowcount. An unconditional increment beside an idempotent insert reintroduces exactly the double count the constraint just prevented, which is why ON CONFLICT DO NOTHING without RETURNING is the wrong shape: it cannot tell you whether anything changed.
  • Unlike is the mirror: UPDATE engagement SET undone_at_utc = now() WHERE actor_id = $1 AND content_id = $2 AND action_kind = 'like' AND undone_at_utc IS NULL, decrementing on one row affected. Never DELETE - the row is the ledger the counter is reconciled from, and deleting it also erases the evidence that the retry was a retry.
  • Name the isolation dependency: ON CONFLICT DO UPDATE takes a row lock, so concurrent duplicates serialise. Under READ COMMITTED the loser proceeds against the winner's committed row; under REPEATABLE READ the same statement raises a serialization failure (SQLSTATE 40001) that the handler must retry rather than surface to the client.
  • Call out the one case the triple does not cover: an unlike retried late, after a genuine re-like, matches on the triple and undoes the newer like. Bound it by retaining the key of the last state transition and rejecting a request whose key was already applied.

Expected result (20 min): Exactly one row for the triple throughout. RETURNING yields one row on the first like, zero on both retries, one on the re-like after the unlike, and zero on a duplicate of a live like, so a rowcount-gated counter records +1, nothing, nothing, -1, +1.

Check yourself

  • COUNT(*) for (actor, content, 'like') is 1 after all requests, whatever their order.
  • The counter equals SELECT count(*) FROM engagement WHERE content_id = $1 AND action_kind = 'like' AND undone_at_utc IS NULL at every step - that query is also the reconciliation job.
  • Under REPEATABLE READ one concurrent session raises 40001 instead of producing a wrong count.

Follow-ups

  • The counter is one row on a viral item and the increment now blocks. Where does it move, and does the answer change the statements above?
  • How long must idempotency_key be retained to cover client retries, and what does dropping it early cost?
  • The same actor likes from two devices within 50 ms. Which statement wins, and does either client see an error?

Model the follow graph for both of its opposite reads

follow_edge holds follower_id, followee_id, created_at_utc, removed_at_utc, edge_state ('active','unfollowed','pending_request','blocked') and is_reciprocal. account holds follower_count and following_count. Two reads matter: which accounts a viewer follows, bounded by that viewer's following count, and who follows an author, unbounded and heavy-tailed. Give the primary key and every secondary index you need, and say why neither read can use the other's index. State exactly what a re-follow does to the existing row, and which of follower_count and is_reciprocal may be trusted for an authorization decision.

How a strong candidate works it

  • Primary key (follower_id, followee_id). A B-tree leading on follower_id turns the viewer's followee list into one range scan returning k rows for a following count of k, and the same index cannot answer a followee_id lookup: with the leading column unbound the planner's only options are a full scan of the index or of the heap, on the largest table in the system.
  • Add the reverse index (followee_id, follower_id) for the publish path. Make it partial on edge_state = 'active', because soft-deleted rows stay forever and fanout never wants them, with the precondition that the query repeats that predicate literally or the planner will not match the partial index. The cost is a second index entry per edge plus maintenance on every state change.
  • A re-follow updates in place, because the primary key forbids a second row: INSERT ... ON CONFLICT (follower_id, followee_id) DO UPDATE SET edge_state = 'active', removed_at_utc = NULL. Decide what created_at_utc means and say it out loud - keeping the first-follow time means one row cannot hold the follow/unfollow history, and if that history is required it belongs in an append-only follow_event table with this row as its current-state projection.
  • follower_count is a cache reconciled from this table, so an authorization test phrased as a count reads a number rather than an edge. Whether a viewer may see a followers-only item is an existence check against follow_edge with edge_state = 'active', which the reverse index answers in one lookup.
  • is_reciprocal is written by whoever inserts the opposite edge, so it is stale for the window between the two writes and permanently stale if that writer failed. It is a display hint; maintain it in the same transaction as the opposite edge or drop the column and answer reciprocity with a lookup on the second index.

Follow-ups

  • Blocks live in this table as edge_state = 'blocked'. What breaks when a block and a follow must coexist between the same pair, and would you split blocks out?
  • The fanout worker has to page a 40-million-row follower list. What is the cursor, and why not OFFSET?
  • Reconciliation finds follower_count off by 12 for one account. Write the query that finds the drift, and say whether the drift ever means the edge table is wrong.

Choose the fanout threshold that fits a daily write budget

You have one day of publishes as (author_id, publish_count) for up to 40 million authors, plus each author's follower_count, which reaches 10^8. Under push fanout a publish costs one timeline write per follower; under pull it costs zero. A threshold T makes every author with follower_count at or above T pull, and everyone below it push. Given a daily budget B of timeline writes, return the largest T that keeps total writes at or below B, and the fraction of publishes served by pull at that T. The obvious solution is correct; say why it is too slow, then replace it.

How a strong candidate works it

  • Price the naive method precisely instead of waving at it. Evaluating one candidate T is a full O(A) pass summing publish_count times follower_count over authors below T; sweeping every possible follower value is up to 10^8 candidates, so it is O(A * 10^8), and even restricting candidates to the distinct observed follower counts leaves O(A^2) in the worst case. It is correct and it will not finish.
  • Establish monotonicity and get its direction right: raising T moves more authors into push, so total writes is non-decreasing in T. That is why the question asks for the largest T within budget rather than the smallest; the smallest is trivially the one where nobody is push, and it wastes the read path.
  • Sort authors by follower_count once, O(A log A), and build a prefix sum of publish_count times follower_count over that order. total_writes(T) is then one array lookup at the boundary index for T, and the answer is the last prefix entry at or below B, found by a single linear scan of the prefix array. No binary search is needed unless you must answer many budgets against the same day.
  • Watch the arithmetic width, because it decides the answer rather than merely the tidiness. Per-author products reach 10^10 and the running total passes 2^53, above which a float64 no longer represents consecutive integers, so accumulating in doubles moves the chosen T. Use 64-bit integers and say so.
  • Compute the pull publish fraction from a second prefix sum over publish_count alone, taken in the same sorted order and the same pass, so the two answers cannot disagree about where the boundary sits.
  • State the limit of the result: this optimizes the write budget only. The read path pays a k-way merge per viewer proportional to how many pull-mode accounts that viewer follows, so T must also be checked against the worst-case following count, and the usable answer is the intersection of both constraints. Add that because follower counts are heavy-tailed, the prefix curve is nearly flat and then jumps, so T is insensitive over a wide range and then abruptly sensitive: report the curve, not just the number.

Follow-ups

  • Writes per day is the wrong unit, because the queue fails on instantaneous backlog rather than a daily total. How do you restate the budget, and what does that do to the data you need?
  • T changes. What happens to the timelines already materialized for an author who just flipped to pull, and what is missing for one who just flipped to push?
  • One author has 10^8 followers and publishes 200 times a day. Show what that single author does to your answer, and argue whether a global threshold is the right instrument for them at all.

Flag actors exceeding a rate ceiling in any sliding window

An event stream gives you (actor_id, action_kind, ts_ms) for engagement writes, already sorted by ts_ms: up to 200 million events over one day from up to 30 million distinct actors. Return every actor_id that at any point had more than K actions inside a window of W milliseconds, where K is at most 1000 and W at most 60000. The window slides continuously; it is not a fixed bucket. One pass over the stream. State time and space bounds, and say how memory stays bounded given that most of those actors are idle at any instant.

How a strong candidate works it

  • Reduce the per-actor state to the minimum that can answer the question: you never need all of an actor's timestamps, only the K-th most recent one. Keep a K-slot ring buffer per actor; on each event, if the buffer is already full, compare ts against the oldest slot, and flag when the difference is within W, because that means K+1 events fall inside one window.
  • Say why fixed tumbling buckets of width W are wrong rather than merely approximate: K events at the end of one bucket and K at the start of the next are 2K events inside a single W-wide window and never trip a bucketed counter. The undercount is structural, not a tuning issue.
  • Bound memory by eviction, not by capacity: an actor whose most recent event is older than W can never contribute to any future window, so drop its buffer. Maintain a FIFO of (actor_id, ts) appended on every event and pop from the front while the front ts is older than now minus W, discarding an entry whose ts no longer matches that actor's latest event. That lazy-deletion pop is amortized O(1).
  • Give the bounds in terms of the right variable: O(n) time overall, and O(A_w * K) space where A_w is the number of distinct actors active within any W-millisecond window, which at W = 60 seconds is orders of magnitude below 30 million. Quoting space as O(actors) instead of O(active actors) is the answer that makes this look infeasible when it is not.
  • Fix the boundary convention before coding it. State whether the window is half-open, so that an event exactly W after the K-th previous one does not flag, and keep that convention identical in the eviction test, or the detector disagrees with itself at the edge.

Follow-ups

  • The stream is now unsorted by up to 5 seconds of clock skew. What breaks first, and what is the minimum buffering that restores a correct answer?
  • You are given a fixed memory budget that may not grow with the active actor count. What structure do you reach for, and which direction does its error run: false flags or missed ones?
  • The product wants the count of distinct content items acted on in the window rather than the count of actions. What in your per-actor state has to change, and what does that do to the space bound?

Size encoder concurrency and schedule transcodes for makespan

Transcode jobs are (asset_id, rendition_profile, ready_at_ms, duration_ms): up to 2 million a day, durations from 200 ms to 40 minutes. Part one: given a log of jobs that actually ran, each with (start_ms, end_ms) where the interval is half-open, report the minimum number of concurrent encoder slots that would have been needed and the first instant that peak was reached. Part two: given C slots, with no job starting before its ready_at_ms and no preemption, assign jobs in ready_at order and report the makespan. State the complexity of each part.

How a strong candidate works it

  • Recognize part one as maximum interval overlap and solve it with a sweep over 2n signed endpoint events sorted by time: +1 at a start, -1 at an end, running sum, track the maximum and the first time it is attained. O(n log n) for the sort, O(n) space.
  • Get the tie rule right, because it is the whole correctness of the sweep: with half-open intervals, an end at time t must be processed before a start at time t, otherwise every pair of back-to-back jobs inflates the peak by one and you over-provision the pool. Encode it in the sort key, not in a comment.
  • Note the equivalent formulation with the same bound and less machinery: sort start times and end times into two arrays and two-pointer them, advancing whichever is smaller with ends winning at equality. Same O(n log n), constant extra space beyond the two arrays.
  • Solve part two with list scheduling on a min-heap of C slot free-times: for each job in ready_at order, pop the earliest free time f, start at max(f, ready_at_ms), push back start + duration_ms. O(n log C) time, O(C) space, and the makespan is the maximum value remaining in the heap.
  • Bound the claim you make about part two, because this greedy is not an optimizer: it reports the makespan of one specific, reproducible policy, the non-delay schedule induced by ready_at order, and that number can be strictly larger than what the same jobs achieved on the same number of slots in a real log. What is provable about it is an approximation bound. The last-finishing job j waits only when every slot is busy throughout [ready_at_j, start_j) -- no slot can idle there, since every job placed before j has ready_at at most ready_at_j -- so the total work is at least C * (start_j - ready_at_j) + duration_j. Combine that with OPT at least (sum of durations) / C and OPT at least ready_at_j + duration_j and you get makespan at most 2 * OPT - duration_j / C. The bound is close to tight: with all ready_at zero, C * (C - 1) unit jobs followed by one job of length C gives exactly (2 - 1/C) * OPT.
  • Say what minimizing it would actually cost, so the greedy is a choice rather than an oversight: reordering to minimize makespan on C identical slots is NP-hard for C at least 2, since P2||Cmax contains PARTITION, and longest-processing-time-first is the classic 4/3 - 1/(3C) approximation for the case without release times.
  • Connect the two numbers to the decision they inform: part one sizes the pool for a no-queueing service level, part two gives the drain time at a chosen C, and the gap between them is the queueing delay a publish-time rendition inherits, which is what the product actually feels. Do not close the loop by asserting that C set to part one's peak reproduces the log's end time. The log's own schedule may have used an assignment this greedy will not choose, so the greedy makespan at that C can be strictly larger.

Expected result (25 min): Part one: the peak equals the maximum prefix sum over 2n signed endpoint events with ends ordered before starts at equal t, plus the first t attaining it, in O(n log n) time and O(n) space. Part two: the makespan equals the maximum residual free time after a min-heap pass in ready_at order, in O(n log C) time and O(C) space, reported as the makespan of that one non-delay policy -- at most 2 * OPT - duration_j / C, not the optimum, and not guaranteed to match the original log's end time even when C is set to part one's peak.

Check yourself

  • Two jobs [0,10) and [10,20) report a peak of 1, not 2.
  • With C at least the number of jobs, part two returns max(ready_at_ms + duration_ms), which is checkable directly from the input.
  • With C = 1 and all ready_at_ms zero, the makespan equals the sum of all durations.
  • Part two on three jobs with ready_at_ms 0 and durations 2, 2, 3 at C = 2 returns 5, while the log [0,2), [2,4), [0,3) of those same three jobs has a peak of 2 and ends at 4: feeding the peak back as C can yield a makespan strictly later than the log's last end_ms, and an implementation that returns 4 here is not doing list scheduling in the given order.

Follow-ups

  • Live publish renditions and a classifier reprocessing backfill share the pool. How do you keep the backfill from occupying every slot without letting slots idle when there is no live work?
  • A job crashes at 90 percent and is retried. Does your peak-concurrency figure count that as one interval or two, and which does capacity planning want?
  • Durations are estimates that can be off by 3x. Which of your two answers degrades under that error and which does not?

Page a materialized home timeline with a stable cursor

A mobile client requests a viewer's home timeline, 30 items per page, at 500,000 page requests per second peak. Entries live in timeline_entry (owner_id, content_id, author_id, inserted_at_utc, sort_key, source) and new entries arrive at the head continuously while a viewer pages. Specify the pagination contract: the query for page one and for page N, what the cursor contains, the index that serves it, and what happens when two entries share a sort_key. A page must never repeat an item the viewer already saw or silently skip one.

How a strong candidate works it

  • Rule out OFFSET on two separate grounds: it defines the window by counting from the start of a result set that mutates between requests, so k insertions at the head repeat k rows on page two and deletions skip the same number with no signal; and the engine must produce and discard OFFSET rows first, making page n cost O(offset + k) rather than O(log N + k).
  • Define the cursor as the values of the last row returned, not a position: page one is ORDER BY sort_key DESC, content_id DESC LIMIT 31, page N adds the row-value predicate (sort_key, content_id) < (cursor_sort, cursor_content), which PostgreSQL can satisfy as a single index seek.
  • Back it with an index on (owner_id, sort_key, content_id) so the ordering is produced by the scan and no Sort node appears. A uniformly descending read can be served by scanning that index backwards, so explicit DESC in the DDL only matters if the sort directions are mixed.
  • Make the key unique within the owner: sort_key is the time-sortable content_id, and (owner_id, content_id) is already the primary key. A wall-clock timestamp collides at high insert rates, and at a tie the boundary row is either returned twice or dropped, which is exactly the failure keyset pagination was adopted to remove.
  • Fetch limit + margin because visibility filtering removes rows after the read, derive has_more from whether the surplus survived filtering rather than from row count, and return the cursor opaque and signed so a client cannot forge a position into another owner's timeline.

Expected result (20 min): Page one: SELECT ... WHERE owner_id = $1 ORDER BY sort_key DESC, content_id DESC LIMIT 31. Page N: the same with AND (sort_key, content_id) < ($2, $3). Index: (owner_id, sort_key, content_id). Thirty rows are returned; the thirty-first is consumed only to set has_more. Under OFFSET 30 the five head insertions cause page two to repeat content_ids 875 through 871.

Check yourself

  • EXPLAIN shows an Index Scan or Index Only Scan with no Sort node above it.
  • Inserting rows at the head between requests leaves page two byte-identical under the keyset query.
  • Deleting the exact row the cursor was taken from still yields a correct page two, because the predicate compares values rather than identity.
  • Concatenating all pages yields each content_id at most once.

Follow-ups

  • The viewer pulls to refresh and wants items newer than the head of page one. What is the query, and does it share the cursor contract?
  • Replace sort_key with a ranking score that is recomputed between requests. Which part of the guarantee survives and which does not?
  • The timeline store is wiped and rebuilt while a client holds a cursor. What does that client see on its next request?

Own the outage where one fanout starved every other author

Every author's timeline writes share one fanout queue. A publish by an account with roughly 12 million followers enqueued its entire follower fan, and for 41 minutes items from ordinary authors stopped reaching feeds while the backlog drained. Nothing errored: publishes returned 200, the read path was healthy, and error-rate dashboards stayed flat. Take the on-call role. Describe an incident you owned with a comparable silent blast radius: how it was detected, how you bounded who was affected, what you stopped first, and the one metric that sized the damage. Include what you got wrong during the response.

How a strong candidate works it

  • Open with the invariant that broke, stated in a measurable form: a published item is materialized into follower timelines within N seconds at p99. The symptom (feeds look stale) tells a listener nothing countable; the invariant names both the damage and the metric.
  • Explain why nothing errored, because that is the interesting part. An at-least-once queue degrades by lag, not by failure, so the detector has to be the age of the oldest unprocessed publish event per partition. Queue depth alone hides this case: depth is small when one task is enormous.
  • Bound the population with two numbers you can derive, not an adjective. Publishes enqueued behind the stalled fan is a count from the queue; readers affected is the sum of account.follower_count over those authors. Say which one you actually computed during the incident and which you reconstructed after.
  • Separate mitigation from fix and say which came first. Mitigation is to stop paying the cost now: flip the offending author to pull mode or move that fan to an isolated partition so small fanouts drain. State its immediate cost, which is a read path that must merge pushed and pulled items mid-incident. The threshold and per-size partitioning are the fix and are not an incident-window change.
  • Say what redelivery did while you drained. Because a timeline write is an upsert on (owner_id, content_id), replayed events rewrote the same slots and the recovery was re-runnable; if the write had been an append, draining would have doubled entries in every timeline it touched.
  • Name one thing you got wrong inside the response window, such as a mitigation that made the backlog worse or time spent on the wrong hypothesis. Interviewers weight this heavily because it is the part candidates rehearse away.

Follow-ups

  • Your mitigation flipped one author to pull mode. What must the feed read path do differently for viewers who follow that author, and how do you avoid serving them the item twice while already-pushed rows still exist?
  • How would you have detected this in five minutes instead of 41, and what would that detector cost in false pages per week?
  • The queue drained but some viewers were still missing items. Where do you look, and what exactly do you rebuild?

Where candidates lose points

Spending the start of the technical screen on local setup

Candidates report coding in their own IDE over screen share, so any setup problem costs you time in front of the interviewer. The day before, open a fresh project in the language you will use, start a TCP listener on localhost, connect two telnet or netcat sessions, and confirm your firewall allows loopback connections. Bookmark the standard-library docs for sockets, threading and asyncio, and have the command that runs your server ready.

A chat server that crashes or misbehaves when a client leaves

The reported chat prompts ask for a broadcast that does not echo to the sender, and for clean disconnects. The usual bugs are writing to a closed socket (broken pipe) inside the broadcast loop, changing the client set while iterating over it, and treating one recv() as one message. Get the plain broadcast working first. Then handle a zero-byte read and write errors by removing that client under a lock or inside the event loop, and frame input on newlines. Test with three terminals, killing one mid-session, before you add nicknames or user lists.

Leader election that only works on the happy path

In the etcd leader-follower prompt, electing a leader is the easy half. Explain how leadership is held (a lease with a TTL that the leader keeps alive), what followers watch to detect its loss, and what happens when an old leader pauses past its TTL and resumes still acting as leader. Name the guard, such as checking the lease or a revision before acting, and show followers redirecting traffic to the current leader. When you practise, kill the leader process and measure how long failover takes.

Naming a fix before gathering evidence in the incident walk-through

For reported prompts like a CPU spike after a routine deployment, or dropped sockets during a connection burst, first ask what changed and when, which metrics and logs you can see, and who is affected. Then name a cause. Keep mitigation (roll back, shed load, rate limit) separate from root cause, and say what each piece of evidence rules out. Network saturation, thread-pool exhaustion and database lock contention each leave a different signature.

A project retrospective with no decisions of your own and no mistakes in it

Follow-ups in a retrospective can go into architectural missteps, edge cases you missed and friction with other teams. Write the project as a timeline of decisions you made, with the option you rejected at each point, one thing that went wrong and what you changed, and one result you can measure. Replace team-level summaries with a clear statement of your own part.

Weakness triage, driven by a day-one self-assessment

Day one measures instead of guessing, under a fixed rubric, and the remaining hours are allocated in proportion to the gaps before any studying begins. The allocation is deliberately not renegotiated midweek, because the area that feels worst on day three is usually the one that is moving.

Day 1 — Local setup and the first chat server

  • Set up the environment you will interview in: a fresh project in your strongest language, telnet or netcat installed, and a localhost port your firewall allows. Time how long it takes to go from a blank project to a running listener.
  • Build the reported baseline: a multi-client TCP chat server that sends each message to every other client and does not echo it to the sender.
  • Test with three terminal clients, then kill one mid-session and fix whatever breaks, such as a broken pipe or a client set that changes during iteration.

Deliverable: A server you can build from blank and demonstrate with three clients, plus a note of your setup time.

Day 2 — Extend the server the way the follow-ups do

  • Add these one at a time: a nickname prompt on connect, a command that lists online users, join and leave notices, and clean shutdown.
  • Rebuild the same server from blank with the other concurrency model (asyncio if you used threads, or the reverse) and write down one trade-off between the two.
  • Frame input on newlines so a message split across two reads, and two messages in one read, are both handled correctly.
  • Read the bank's DTLS buffer processing and telnet-compatible chat items and list the framing edge cases they raise.

Deliverable: Two working versions of the extended server and a one-paragraph comparison of threads and asyncio.

Day 3 — Distributed coordination: leader election with leases

  • Implement the reported leader-follower cluster against a local etcd: nodes compete for a key held by a TTL lease, the leader keeps the lease alive, and followers watch for its loss.
  • Make followers redirect requests to the current leader, then kill the leader and measure how long failover takes.
  • Write your answer for the stale-leader case, where an old leader pauses past its TTL and then resumes, and compare it with the bank's Redis-lease variant.

Deliverable: A running three-node demo and a short failure table: event, what each node does, and how long recovery takes.

Day 4 — System design: matchmaking and message storage

  • Talk through the reported matchmaking design aloud, starting from requirements: skill tiers, latency limits, wait time, and what you relax as players wait longer.
  • Design message storage for a chat workload on ScyllaDB or PostgreSQL: partition key, ordering key, indexes, and the queries each choice makes expensive.
  • Work the 'Page a materialized home timeline with a stable cursor' exercise, then apply its keyset-cursor rule to paging through a channel's message history.

Deliverable: Two one-page design sketches, each with a requirement list, a data model and three trade-offs.

Day 5 — Incident troubleshooting

  • Run the reported CPU-spike-after-deployment prompt aloud: what changed, which metrics and logs you ask for, how you bound the impact, the mitigation, and the root cause.
  • Do the same for dropped socket connections during a connection burst, telling network saturation, thread-pool exhaustion and database lock contention apart by the signal each one leaves.
  • Review C++ struct padding, alignment, and what breaks ABI compatibility when a client library changes, for the reported fundamentals question.
  • Answer the 'Own the outage where one fanout starved every other author' drill using an incident of your own.

Deliverable: A one-page incident checklist and two recorded walk-throughs that follow it.

Day 6 — Event data and applied product engineering

  • Implement sessionization from an event stream: group each user's events into sessions split by a 30-minute gap, then compute per-channel engagement.
  • Solve the 'Flag actors exceeding a rate ceiling in any sliding window' drill and work the 'Make the like write idempotent and the counter conditional' SQL exercise.
  • If your target team works on clients, build a React message reaction bar with live counts, or a Snake game with a tick loop, keyboard input and collision checks, in your local IDE.

Deliverable: Working sessionization code with a test for a gap exactly at the boundary, plus one finished SQL or UI exercise.

Day 7 — Retrospective, behavioral answers and a full mock

  • Write your project retrospective as a timeline of decisions, with rejected options, one failure and one measured result.
  • Answer the disagreement, deadline trade-off and explaining-to-non-technical-partners prompts aloud and record them.
  • Run a mock of the technical screen: build the chat server from blank while a partner adds follow-up requirements live.

Deliverable: A one-page retrospective and a recorded mock, with notes on where you stalled.