Interview guides · Twitch
Twitch Software Engineer Interview Guide
A prep plan for Twitch software engineer candidates: answer first, read the solution second, repeat the misses later.
The 60-second brief
This guide covers what a Software Engineer at Twitch is expected to do and how to prepare for the interview.
The title spans product, platform and infrastructure work, and which of those the seat actually is decides whether design or algorithms carries more weight in your preparation. The posting rarely settles it; what the team is on call for usually does.
Focus areas
- Sessionise at-least-once heartbeats into exactly-once plays
- Design a bitrate ladder under stated constraints
- Enforce entitlement at the licence boundary
The Twitch interview process
Twitch candidates report 4 rounds · ≈ 3-5 weeks. The stages below are what candidates describe, not a published process.
Recruiter Call
Half of this call is the part candidates treat as small talk: start date, notice period, work authorisation and its timing, location and time zone, on-call, and the number. Those are what kill offers late, after several engineers have each spent a day. Surfacing a hard constraint now costs you nothing and occasionally buys you something, since a loop compressed to fit a competing deadline can usually only be arranged if it is asked for early. The common failure is deflecting the compensation question twice, then discovering at offer stage that the band never reached your number.
What to demonstrate
- Whether your hard constraints are compatible with the role before a loop gets booked: earliest start, notice period, what authorisation you hold and when it needs action, days on site, willingness to carry a pager
- Whether you give a compensation range with something behind it, such as current total compensation or a competing timeline, rather than leaving the band untested
- Whether your stated timeline is real, since a competing deadline raised now is something scheduling can sometimes work around and the same deadline raised at offer stage usually is not
How to prepare
- Write each constraint down in one line before the call and state them as facts rather than negotiating them live under a question you were not expecting
- Set your range from two or three current data points for that level and location, and name the structure you are quoting in, so the number is comparable to the one they are holding
- If another process is running, say where it stands and by when, and ask directly whether this loop can be scheduled inside that window
Technical Assessment
The same problem is scored by two different mechanisms depending on the format, and preparing for one does not cover the other. With a person watching, partial progress is visible and a hint is a correction you can absorb; silence is the expensive failure, because nobody can read a half-written function. With an automated grader there is no partial credit for what you were about to do, nobody to ask, and the worked examples in the prompt are the entire specification. Read them as a contract, down to whether an empty result should be an empty list or no output at all.
What to demonstrate
- In a live session, whether your commentary tracks what your hands are doing, and whether a hint redirects you or gets defended against
- In an automated one, whether you cover the cases the examples do not show, since the hidden cases are where the score moves
- Whether you manage the clock on purpose: abandoning an approach that is not converging while there is still time to write something simpler that finishes
How to prepare
- Have someone hand you a problem and feed you one deliberately wrong hint. Practise testing it against a concrete case instead of accepting or rejecting it on authority.
- Do one timed run a week in a plain browser editor with autocomplete, linting and your own snippets switched off, which is closer to what these environments give you
- For the automated format, write the harness before the solution: a main that feeds the worked examples plus an empty and a single-element case and prints expected against actual, so a wrong submission is caught by you first
Virtual Onsite Loop
Coding rounds mostly set a floor. They decide whether you clear the bar, not where you land on the ladder. Level tends to come out of the design discussion and the ownership stories, so the question worth auditing beforehand is whether the scope you describe matches the scope of the job. Work that stops at your own service, or a story whose hard part was writing the code rather than getting several people to agree on an interface, reads a level below where you think you are interviewing, and that gap is usually resolved downwards.
What to demonstrate
- Whether the largest thing you describe owning ran end to end — the decision, the migration path, the rollout, and what you did when it went wrong — or stopped at the change you merged
- Whether design answers include what you would not build, what you would defer, and what you would measure before committing, rather than only what the boxes are
- Whether a disagreement in a story was settled with something checkable — a benchmark, a prototype, a written proposal — instead of by seniority or by waiting it out
- Whether you can say which calls you made alone and which you escalated, and why the line sat where it did
How to prepare
- Write your largest piece of owned work as a timeline of decisions — who decided what, when, and what you did when the plan broke — then delete every sentence whose subject is "we" and see how much survives
- Take one system you know well and drill the migration answer: how old and new paths run side by side under live traffic, how you compare their outputs, what the rollback is once writes are going to both, and which step you would not automate
- Map each line of the ladder in the job posting to a specific thing you have done, find the line you cannot support, and prepare the closest evidence you have plus an honest account of the gap
Behavioral Assessment
Many of these questions are about something that went wrong, and the grading sits mostly in the hours after you knew. Who found out first, whether that was you or an alert or a user, how long it took you to say it out loud, and whether the people who needed the news got it while they could still act on it. Engineers under-tell this part because it feels like confessing. The pattern it is looking for is the opposite: the quiet fix, an incident absorbed without telling anyone, after which nothing changed and the same failure is still available.
What to demonstrate
- How the problem was found, and whether that route was one you had built or one that happened to you, since a user reporting it first means your instrumentation did not cover that failure
- Whether time-to-detect and time-to-tell are separate numbers in your account and whether you know both, because a fast fix that nobody heard about until the retro is a different answer from a slow one that was announced immediately
- Whether the resolution left something durable behind, a check that fires or a default that changed, rather than depending on people remembering to be careful
- Whether you can say what the failure cost without either inflating it or waving it away
How to prepare
- Reconstruct one incident you were part of as a timeline with clock times: first bad request, first signal, first person who knew, first message outside the team, mitigation, permanent fix. The gaps between those entries are what gets asked about
- Look up the configuration of the signal that caught it, including its evaluation window and threshold. An alert defined on a five-minute aggregate cannot fire until the condition holds across that window, which puts a floor under time-to-detect that has nothing to do with how severe the failure was. Be able to say what that floor was and whether anyone had chosen it deliberately
- Prepare one story where you escalated early and the severity turned out to be smaller than you thought, including what it cost the people you pulled in. Without it, every answer you give about raising alarms is unfalsifiable
Where candidates lose points
Shortening segment duration to cut live latency.
In classic segmented delivery a compliant client starts roughly three target durations back from the live edge, so halving the segment duration does halve that component - and that is the whole of the appeal. It also doubles the request rate for both segments and playlists, and doubles the number of IDR frames, which cost far more bits than the predicted frames around them, so quality at a fixed bitrate drops. Low-latency extensions exist precisely to break this coupling: they deliver sub-second parts within a segment that stays long enough to encode efficiently.
Including the delivery token, session id or any per-user parameter in the CDN cache key.
It looks like the conservative choice, and it behaves perfectly in staging where three people are watching. In production every viewer becomes a distinct cache object, hit ratio collapses toward zero, and the origin absorbs the entire read load at peak - a cost and availability event rather than a slow page. The correct shape is for the edge to verify the token's signature and then strip it from the key, which means the token has to be verifiable at the edge without a callback to any origin service.
Choosing a schema before the access patterns are known
Write the queries first, with their filters, sort orders, cardinalities and which ones sit on the latency-critical path, then design tables and indexes to serve them. An index nothing queries still costs write throughput and storage, and a hot query with no supporting index becomes a full scan that only hurts once the table is big.
Never running a concrete value through the code
Trace one small input and one edge input by hand, index by index, out loud. Re-reading your own code catches design mistakes; walking a real value through it catches the off-by-one, the uninitialised accumulator and the loop that never advances.
Questions & practice
Implement a search autocomplete system for a streaming video search engine that returns the top 3 historical hot queries matching a given prefix.
Medium · data structures and algorithms
Approach
- Walk one small example through your approach before writing the whole thing.
- Choose the data structure from the access pattern, not from familiarity.
- Restate the input: its shape, its size, and what is guaranteed about it.
Follow-up
- How does this change if the input no longer fits in memory?
- Which test case would catch an off-by-one here?
Implement graph traversal strategies (BFS/DFS) to calculate minimum network hops or relationship distance in user social graphs.
Medium · data structures and algorithms
Approach
- Walk one small example through your approach before writing the whole thing.
- Restate the input: its shape, its size, and what is guaranteed about it.
- Name the brute-force solution and its complexity before improving on it.
Follow-up
- Which test case would catch an off-by-one here?
- What is the worst case, and how likely is it on real data?
Given an unbalanced binary search tree, design an in-order iterator without using an external stack container or recursion memory overruns.
Medium · data structures and algorithms
Approach
- Choose the data structure from the access pattern, not from familiarity.
- State the target complexity and say which constraint rules the naive version out.
- Restate the input: its shape, its size, and what is guaranteed about it.
Follow-up
- Which test case would catch an off-by-one here?
- How does this change if the input no longer fits in memory?
Write a function that parses live chat message streams and extracts specific substring tokens while maintaining sorted frequency output in minimal time complexity.
Medium · data structures and algorithms
Approach
- Walk one small example through your approach before writing the whole thing.
- State the target complexity and say which constraint rules the naive version out.
- Choose the data structure from the access pattern, not from familiarity.
Follow-up
- How does this change if the input no longer fits in memory?
- Which test case would catch an off-by-one here?
Choose ladder rungs against a measured bandwidth distribution
Hard · dynamic programming · optimisation · cost modelling
You have 60 candidate encoding rungs sorted ascending by bitrate, and an audience bandwidth histogram of 1,000 buckets measured from delivered throughput. A viewer plays the highest rung whose bitrate is at most 80% of their sustained throughput, and cannot play at all if no rung qualifies. Choose exactly 12 rungs, always including the lowest candidate, maximising total delivered bitrate across the audience. Return the chosen rungs and the objective value, state the complexity, and say why a log-spaced ladder is not automatically the answer.
Approach
- Precompute for each candidate i the bucket at which viewers can first afford it, threshold_i = bitrate_i / 0.8, by binary search over the bucket edges: O(n log m). Build a prefix sum over bucket counts so any viewer-count range is O(1).
- Formulate the contribution: with the selected set sorted, if rung i is selected and j is the next selected rung, every viewer whose threshold falls in [threshold_i, threshold_j) plays rung i, contributing count * bitrate_i. Call it value(i, j), computable in O(1) from the prefix sums.
- Run the DP over (last selected rung, rungs used): f[j][t] = max over i < j of f[i][t-1] + value(i, j), with the lowest rung forced selected and a sentinel j = n+1 closing the final interval. O(n^2 k) = 60^2 * 12, about 43,000 transitions. The point is that the ladder is measured, not that the algorithm is clever.
- Say when the structure buys more: if value satisfies the concave quadrangle inequality, the inner maximisation admits divide-and-conquer optimisation at O(n k log n). That condition depends on the histogram's shape and must be tested on the real distribution rather than assumed.
- Name what the objective leaves out, because the interview point is the modelling: storage and cache footprint scale with rung count, device decode limits make some rungs unusable by whole client classes, and delivered bitrate is a crude proxy for perceived quality. Log spacing is a prior about a smooth unimodal distribution, and measured throughput is usually bimodal, which is exactly where the prior loses.
Worked solution (40 min)
- Write threshold_i and the prefix-sum lookup, and confirm the 80% headroom appears in exactly one place.
- Derive value(i, j) and check it by hand on a three-rung example against a two-bucket histogram.
- Write the DP recurrence with the forced lowest rung and the closing sentinel, and count the transitions at n = 60, k = 12.
- Compare the DP's chosen rungs against a log-spaced ladder on a deliberately bimodal histogram and quantify the difference.
- List the constraints the objective omits and say where each would enter the formulation.
Expected result: Twelve rungs plus the objective value, from an O(n^2 k) DP over sorted candidates with O(1) range counts (about 43,000 transitions at n = 60, k = 12) and the lowest candidate forced into the selection.
Verify it yourself
- Setting k = n reproduces the full candidate set and the maximum achievable objective.
- Deleting a candidate the DP did not select leaves the objective unchanged.
- The objective is non-decreasing as k increases from 1 to n.
Follow-up
- Add a per-device decode cap so one client class cannot use rungs above 1080p. Does the DP still decompose, and along which dimension?
- Throughput varies during playback and the player switches rungs. What does that do to the highest-affordable-rung model, and what would you measure instead?
- Storage and cache footprint become a hard budget rather than a fixed rung count. Which dimension of the DP state changes?
Two child tables that quietly inflate a watch-time report sixfold
Medium · join fan-out · pre-aggregation · distinct aggregates
playback_session(id UUID PK, account_id, content_version_id, played_ms BIGINT, started_at, cdn_pop). license_grant(id, session_id, key_id, issued_at, status) has several rows per session because live channels rotate keys. qualified_play(id, session_id, content_version_id, status ENUM('pending','counted','reversed'), qualified_at) records a correction as a second row rather than an update. A daily report joins all three and reports, per content version: sessions, total played_ms, licences issued and counted plays. Total watch time comes out roughly six times too high. Explain the mechanism, write the corrected query, and say why COUNT(DISTINCT ...) is not a complete fix.
Approach
- Count the rows the join actually produces: one per (session x grant x play). A live session with six key rotations whose play was reversed and re-counted contributes 6 x 2 = 12 rows, so
SUM(ps.played_ms)adds that session's watch time twelve times. The multiplier varies per session, which is why the result is 'roughly six times' rather than exactly anything — there is no constant to divide out. - Fix the counts with
COUNT(DISTINCT lg.id)andCOUNT(DISTINCT qp.id) FILTER (WHERE qp.status = 'counted'), and price it: distinct aggregates sort or hash per group, so the planner gives up the cheap hash aggregate it would otherwise use. - State why the sum cannot be rescued the same way.
SUM(DISTINCT ps.played_ms)deduplicates by value, so two different sessions that each played exactly 1,800,000 ms collapse into one contribution. Deduplicating a measure by its value is wrong in general; the only correct move is to reduce each session to one row before summing. - Write the corrected shape: aggregate each child to session grain independently, then join.
WITH g AS (SELECT session_id, count(*) AS grants FROM license_grant GROUP BY 1), p AS (SELECT session_id, count(*) FILTER (WHERE status = 'counted') AS counted FROM qualified_play GROUP BY 1) SELECT ps.content_version_id, count(*) AS sessions, sum(ps.played_ms) AS played_ms, coalesce(sum(g.grants), 0) AS grants, coalesce(sum(p.counted), 0) AS counted FROM playback_session ps LEFT JOIN g ON g.session_id = ps.id LEFT JOIN p ON p.session_id = ps.id GROUP BY 1. Every join is now one-to-at-most-one, so no row is duplicated. - Compare the cost: two hash aggregates over the child tables plus two hash joins, each linear in rows scanned, against a plan that first materialises the product of the two children and then aggregates the larger intermediate. Keep the joins LEFT with COALESCE so a session that started but never reached a licence stays in the session count — an INNER join drops it silently and changes what the report means.
- Say which column carries the highest correctness bar:
played_msfeeds reporting, the counted play feeds payout, and a duplicate there is money moved from one rights holder's share to another's. The bar differs by column inside a single report.
Follow-up
- The same report is wanted broken down by CDN POP and device class. Does the pre-aggregation survive, or does each CTE need the extra key?
- A play was reversed and later re-counted. Should the report show one counted play or zero, and stamped at which timestamp?
- How would you catch this class of bug in CI, given the naive query returns a plausible number rather than an error?
Serve a rights window where a null end means perpetual
Easy · null semantics · range predicates · index column order
availability_window(id, content_version_id, territory_code CHAR(2), licence_type ENUM('subscription','ad_supported','rental','purchase'), starts_at, ends_at NULL, max_resolution_cap NULL, status ENUM('draft','active','withdrawn')) carries INDEX (content_version_id, territory_code, starts_at, ends_at). A null ends_at means perpetual. Playback start asks: may this content version be served in this territory, on this licence type, at this instant, and under what resolution cap? Write that query. Then say which index columns actually narrow the scan and which are only filters, and what should happen when one row's ends_at equals another row's starts_at.
Approach
- Name the failure in the naive predicate first.
:now BETWEEN starts_at AND ends_atexpands to:now >= starts_at AND :now <= ends_at, and on a perpetual row the second comparison is:now <= NULL, which is unknown rather than true. Three-valued logic drops the row with no error, so every perpetual grant becomes unplayable and nothing in the system complains. - Write it as a half-open interval with an explicit null branch:
status = 'active' AND content_version_id = :cv AND territory_code = :terr AND licence_type = :lt AND starts_at <= :now AND (ends_at IS NULL OR ends_at > :now). Half-open means a renewal row starting exactly when the previous row ends matches exactly one row at the boundary instant rather than two or zero. - Read the index honestly.
content_version_idandterritory_codeare equalities and set the scan range;starts_at <= :nowis the first inequality and closes it.ends_atsits after a range column, so in a b-tree it cannot narrow the scanned range at all — it is evaluated against each index tuple in the range.licence_typeis not in the index, so it is a filter after the fetch; putting it beforestarts_atwould make it a third equality. - Put the index in proportion: a single (content version, territory) pair has a handful of window rows, so the range is tiny whatever the column order. The index exists so the lookup does not touch other titles, not to make the range tight — say that rather than over-tuning a predicate whose selectivity is already a few rows.
- Decide the multi-match case deliberately. Subscription and rental windows for the same content version can both be active, which is legitimate; two active rows for the same triple are the overlap data bug. The serve path should take the most restrictive
max_resolution_capand raise, because picking arbitrarily means two viewers in one territory get different caps from the same data.
Worked solution (20 min)
- Insert four rows for one content version: an active perpetual subscription row, an active rental row that ended yesterday, a draft row covering today, and an active row for a different territory.
- Run the BETWEEN form and record which rows come back.
- Rewrite with the half-open predicate and the explicit null branch, re-run, and confirm only the perpetual subscription row matches for the subscription licence type.
- Add a renewal row whose
starts_atequals the expired rental'sends_at, then query at exactly that instant for the rental licence type. - Run EXPLAIN on both forms and note which columns appear as index conditions and which as filters.
Expected result: The BETWEEN form returns zero rows for the perpetual grant. The half-open form returns exactly one row for the requested licence type, and at the boundary instant exactly one of the expired and renewal rows matches. EXPLAIN shows the two equality columns plus starts_at bounding the scan, ends_at listed as an index condition but not narrowing it, and licence_type applied after the fetch.
Verify it yourself
- The perpetual row is returned by the rewritten query and by no BETWEEN variant.
- At the boundary instant, the count of matching active rows for one (content version, territory, licence type) is exactly 1 — not 0 and not 2.
- A
status = 'draft'row never appears, whatever its dates say. - EXPLAIN (ANALYZE, BUFFERS) shows the scan reading only this content version's index entries, not the whole index.
Follow-up
- What constraint would stop two overlapping active rows for the same (content_version_id, territory_code, licence_type) from being written at all?
- The contract says the window closes at midnight local to the territory, but
starts_atandends_atare instants. Where does the local-time-to-instant conversion live, and what happens in a territory that shifts its clocks? - How stale is a cached answer to this query allowed to be, given that the cache TTL is the exposure window for a rights change?
Design a generic real-time analytics SDK for client devices (web and mobile) that buffers, batches, and reliably transmits telemetry data.
Medium · system design and architecture
Approach
- Name the failure you are designing for, then the recovery path.
- Choose a partition key and say what query it makes expensive.
- Fix the scope first: who calls this, how often, and what they do when it fails.
Follow-up
- How does this behave when that dependency is down for an hour?
- What would you drop to keep the system up under load?
Design a global Content Delivery Network (CDN) deployment strategy featuring multi-region data routing and global load balancing on AWS.
Medium · system design and architecture
Approach
- Name the read and write paths separately; they rarely have the same bottleneck.
- Fix the scope first: who calls this, how often, and what they do when it fails.
- Name the failure you are designing for, then the recovery path.
Follow-up
- How does this behave when that dependency is down for an hour?
- What breaks first when traffic grows ten times?
Given a partial codebase, implement a feature on top of existing classes while preserving encapsulation and object-oriented best practices.
Medium
Approach
- Work from the requirement backwards to the design.
- Say what you would check first and why it is the highest-information step.
- State your assumptions explicitly before working the problem.
Follow-up
- What assumption would you test first?
- How would you know your answer was wrong?
Cut live latency to three seconds without starving the buffer
Hard · latency-budget · playlist-scaling · encoder-failover · request-coalescing
A live channel runs at roughly 30 seconds glass-to-glass with 6-second segments and a 4-hour DVR window, serving 100,000 concurrent viewers. The target is 3 seconds. Decompose the current 30 seconds term by term, decide between shortening segments and delivering partial segments with the cost of each, compute the playlist bytes a viewer refetches over that DVR window and cut them, and say what a viewer sees when the primary encoder dies and the standby takes over mid-event.
Approach
- Decompose before optimising, because two of the terms are fixed by the delivery model rather than by tuning. Capture and encode is roughly 1 to 2 s; packaging cannot publish a segment until it is complete, which is a full 6 s; edge propagation is a few hundred milliseconds; and a compliant client starts about three target durations from the live edge, which is 18 s. Those four sum to roughly 26 s against an observed 30, and the residual is contribution and ingest you should measure rather than assume. It also identifies the client buffer and the publish term, together 24 s, as the only places worth attacking.
- Reject shorter segments on the arithmetic. Halving to 3 s halves both the buffer and the publish term to about 14 s total, doubles segment and playlist request rate, and doubles IDR count; IDR frames cost far more bits than the predicted frames around them, so quality at fixed bitrate falls. Reaching 3 s this way would need roughly 1 s segments, where the buffer term alone is 3 s and the request rate is six times the original. The coupling between latency and segment duration is what has to break.
- Break it with partial-segment delivery. Keep the 6 s segment so encoding efficiency and IDR spacing are preserved, publish 0.5 s parts within it, and let the client sit about three parts back. The budget becomes roughly 1 s encode plus 0.5 s part publish plus 0.3 s propagation plus 1.5 s client buffer, near 3.3 s. Be honest that request count still rises with part rate; preload hints with chunked transfer of the open part and connection multiplexing recover most of it, but the saving versus short segments is in bits, not in requests.
- Fix the playlist before it becomes the largest object served. A 4-hour window at 6 s is 2,400 entries, around 240 kB at roughly 100 bytes per entry. In low-latency operation a client reloads about once per part, so 240 kB x 100,000 viewers every 0.5 s is 4.8 x 10^10 bytes per second, about 384 Gbps of near-identical playlist text against 500 Gbps of actual media. Delta updates cut the response to a recent window, and the skip boundary has to be at least six target durations, so about 36 s of entries plus their parts, a few kilobytes instead of 240 kB and a reduction of roughly fifty to a hundred times.
- Treat the blocking playlist request as a capacity unit of its own. Clients ask for a part that does not exist yet and the server holds the request until it does, so a POP's capacity is measured in concurrent held requests and file descriptors rather than in requests per second. Coalesce held requests for the same future part at the shield so the origin holds one request per part rather than 100,000, and put a deadline on the held request so a stalled encoder returns a current playlist instead of timing every client out at once. Note the contrast with segment caching: the msn and part query parameters identify different responses and belong in the playlist cache key, unlike a delivery token.
- Make failover a redundancy problem, since a missed encode deadline cannot be retried once the audience has played past it. Run redundant encoders locked to the same timecode source with identical fixed-GOP settings and the same pinned build, so both produce boundaries at the same instants and the standby's output splices cleanly. Without that lock the timeline breaks and the manifest needs a discontinuity, which forces players to reinitialise the decoder: a visible glitch on many devices and a dropped session on some. The whole mitigation is bought before the failure, not after it.
Worked solution (45 min)
- Write the five latency terms with numbers at 6-second segments and confirm they sum near the observed 30 seconds.
- Recompute the same terms at 3-second segments and at 6-second segments with 0.5-second parts, and state the request-rate and IDR cost of each.
- Compute playlist size, per-viewer refetch rate and fleet playlist bandwidth, then recompute both with a delta update at the minimum skip boundary.
- Specify shield coalescing for blocking requests, the held-request deadline, and the playlist cache key including msn and part.
- Describe the redundant encoder setup and trace exactly what a mid-event failover looks like with and without a locked timecode.
Expected result: A five-term budget totalling about 28 seconds with client buffer and publish identified as the 24 seconds worth attacking, partial-segment delivery chosen over shorter segments with the bits-versus-requests trade stated, a budget near 3.3 seconds, playlist bandwidth cut from about 384 Gbps to single-digit Gbps by delta updates at a skip boundary of at least six target durations, shield coalescing of blocking requests with a deadline, and timecode-locked redundant encoders so failover needs no discontinuity.
Verify it yourself
- The decomposed terms at the current configuration account for the observed glass-to-glass figure, with any residual attributed to a named term you have measured rather than to rounding.
- The proposed configuration's terms sum to the target, and each term is attributable to a named component.
- Playlist bandwidth after delta updates is a small fraction of media bandwidth rather than comparable to it.
- A forced encoder failover in a rehearsal produces no discontinuity tag and no decoder reinitialisation on a sample of player classes.
Follow-up
- Half your audience is on players without low-latency support. How do you serve both from one origin without doubling anything?
- A POP's held-request count saturates 20 minutes into the event. What do you shed first, and what does the viewer experience?
- The standby encoder is a build newer than the primary. What do you check before allowing it to take over?
Session service leaks two gigabytes a day, restarts nightly
Medium · memory leak · retention · ttl eviction
The playback session service holds about 400,000 concurrent sessions per instance and is restarted nightly because RSS grows roughly 2 GB a day; instances OOM within three days if left alone. Old-generation pauses lengthen through the day while throughput is unaffected until the end. Sessions are created at start, renewed by 10-second heartbeats, and removed on an explicit stop; playback_session.ended_at is null for every session whose client simply stopped reporting. Give the ordered checklist and name the leak.
Approach
- Split the growth before choosing a tool: plot RSS minus heap used. A rising heap live set is a reachability leak; RSS rising against a flat heap sends you to off-heap buffers, native TLS state, thread stacks or a pool that never shrinks.
- Read GC logs next. In steady state the live set after each old collection should be flat. A monotonically rising post-collection floor is the leak signature; lengthening pauses with a flat floor is fragmentation or sizing, which is a different fix.
- Take two heap dumps hours apart and diff the dominator tree rather than the class histogram. The histogram names what grew; the dominator names what is holding it, which is the only actionable answer.
- Expect a map keyed by session id holding per-session state, plus a scheduled renewal task per entry. Cross-check its size against the count of sessions with status 'active' whose last_heartbeat_at is older than the lease TTL. A client that stops reporting never sends a stop, so a removal path conditioned on an explicit end never runs for it, and the entry count tracks exactly that population.
- Fix by evicting on the same TTL that governs the lease rather than on an explicit end: time-based expiry plus a sweeper, with the scheduled task cancelled on eviction, since an orphaned timer keeps its captured state alive by itself and will survive a fix that only clears the map.
- Restate the invariant so it does not regress: in-process session state must not outlive the lease it mirrors. The lease store already expires these entries; the in-process copy diverged from it.
Follow-up
- How do you bound the map so that exhaustion becomes a rejected session start instead of an OOM?
- Eviction now also removes sessions whose client is briefly offline - what does the viewer experience, and how does that interact with the lease TTL?
- Which metric would have caught this at 5% of heap rather than at OOM?
Suggested practice checklist
For someone who has spent the last few years shipping features and reading other people's code, and who has not solved a timed problem from a blank file in a long time. Five days rebuild the primitives and the patterns that sit on them, working from invariants rather than remembered solutions, and the last two attach that back to the rest of the loop.
Day 1 — Rebuild the primitives by implementing them
- Implement a dynamic array with doubling growth and an operation counter, then change the growth rule to add a fixed sixteen slots instead, and time both for n of ten thousand, a hundred thousand and a million. The fixed-increment version resizes n/16 times at O(n) each, so its total work is quadratic; doubling is what makes append amortised constant.
- Implement a hash map with separate chaining and a load-factor resize, then insert ten thousand keys engineered to land in one bucket and record what happens to lookup time, so that average-case O(1) becomes a claim with a stated precondition rather than a reflex.
- For dynamic-array append and hash-map insert, write down which cost is amortised rather than worst-case, which single operation pays the whole bill, and what a system with a hard per-operation deadline would have to do instead.
Deliverable: Two working implementations plus a timing table showing the input at which each structure's advertised complexity stops holding.
Day 2 — Arrays under an invariant: two pointers, sliding window, binary search
- Solve longest-subarray-with-sum-at-most-K using a sliding window, then run it on an input containing negative numbers and watch it return the wrong answer: extending the window only moves the sum monotonically when every element is non-negative, and that precondition is the whole reason the technique works.
- Write the binary search that finds the first index satisfying a predicate rather than an exact value, put the loop invariant above the loop in a comment, and verify termination on the two inputs that break careless versions: the empty range, and a range where every element satisfies the predicate.
- Compute the midpoint as lo + (hi - lo) / 2 and write one line on why the obvious (lo + hi) / 2 is a genuine defect in a fixed-width integer type and a non-issue in a language with arbitrary-precision integers.
Deliverable: Three solved problems, each with its invariant written above the loop, plus one recorded input on which the sliding window is provably wrong.
Day 3 — Sorting, heaps, and the greedy argument that has to be proved
- Solve one top-k problem three ways, by full sort, by a size-k heap, and by quickselect, then write the values of n and k at which each becomes the right choice, along with quickselect's quadratic worst case and why a randomised pivot makes that unlikely rather than impossible.
- Implement bottom-up heapify and count sift-down steps to confirm it does linear work rather than n log n, because most nodes sit near the bottom of the tree and therefore move only a short distance.
- Take interval scheduling by earliest finishing time and write the exchange argument out in full: given any optimal schedule, swapping in the earliest-finishing interval keeps it feasible and no smaller. Then construct the weighted variant where that same greedy fails and name what has to replace it.
Deliverable: A three-way top-k comparison with measured crossover points, one written exchange argument, and one counterexample to a greedy rule that looks almost identical.
Day 4 — Recursion, memoisation, and the step to a table
- Take one problem with overlapping subproblems, such as edit distance or coin change, instrument the plain recursion with a call counter to show the blow-up, then add memoisation and re-count.
- Convert the memoised version to a bottom-up table and state the two properties you relied on: each subproblem's result depends only on its arguments, and the dependencies form a DAG you can enumerate in order.
- Rewrite one deep recursion with an explicit stack, then find the input length at which the original hits the interpreter's frame limit, which defaults to about a thousand frames in CPython, so you know when the rewrite is required rather than decorative.
Deliverable: One problem in three forms, naive, memoised and tabulated, with call counts for each and the input length at which recursion depth becomes the binding constraint.
Day 5 — Graphs, where most of the work is choosing the traversal
- Implement BFS and DFS over one adjacency list, then answer for each which finds a shortest path in an unweighted graph and which you would use to detect a cycle in a directed graph, including why the in-progress versus finished distinction matters for the second.
- Implement topological sort by in-degree, feed it a graph containing a cycle, and confirm the failure signature is that fewer than V nodes come out rather than an exception, then note that the order it produces is one of several valid ones.
- Run a shortest-path search on a graph with a single negative edge weight and show the wrong answer, then write the precondition Dijkstra actually needs, non-negative weights, because it finalises a node's distance the first time that node is popped, and name the algorithm you would switch to and its own limit.
Deliverable: A small graph library with BFS, DFS and topological sort, plus two inputs that produce documented wrong answers under the wrong algorithm choice.
Day 6 — One day for everything that is not an algorithm
- Sketch one system only to the depth a coding-heavy loop tends to reach: the endpoints, what the service stores, and the single query pattern that decides the schema. Stop at twenty-five minutes.
- Prepare the project answer for an interviewer who codes, which means rehearsing the two levels they push to: the specific thing you built, and why you chose that approach over the alternative they will name. Open with a number and be ready to say what it excludes.
- Prepare the answer to what you would do differently, choosing a real technical mistake with a specific fix rather than a complaint about process or staffing.
Deliverable: One design sketch at endpoint-and-schema depth, plus a project answer rehearsed to two levels of follow-up.
Day 7 — Solve out loud, under time
- Do three timed problems at twenty-five minutes each in a plain editor with no autocomplete and no execution until the end, then tally separately the failures that were syntax and the ones that were approach, because those two numbers call for different fixes.
- Narrate one solution from the first sentence, stating the approach and its complexity before writing any code, and rehearse the sentence you will use when you realise mid-solution that the approach is wrong.
- Re-solve from blank the two problems you were slowest on this week and compare the times against the day they first appeared.
Deliverable: A recording of one fully narrated solution and a tally that separates syntax failures from approach failures.
The estimate and the day you knew
A slipped date is only a bad story if you sat on it. What matters is what you believed when you gave the number, the signal that told you it was wrong, how many days passed before you said so, and what you cut rather than asking for more time. Scope you defended counts as much as scope you dropped.
Tell me about a product feature you delivered that failed or underperformed. What were your key takeaways, and how did you adjust?
Medium · behavioural and engineering judgement
Approach
- Close with what you would do differently, concretely.
- State the situation in two sentences and spend the rest on the reasoning.
- Give the blast radius: what could have broken, and what you measured.
Follow-up
- How did you know your change caused the improvement?
- What would you do differently if you ran that again?
Describe a complex technical challenge you solved that directly improved customer experience or operational productivity.
Medium · behavioural and engineering judgement
Approach
- Close with what you would do differently, concretely.
- State the situation in two sentences and spend the rest on the reasoning.
- Pick a story where you made the decision, not one where you watched it.
Follow-up
- What did you decide not to do, and why?
- What would you do differently if you ran that again?
Ship a catalogue launch carrying named, dated purge debt
Medium · technical debt · cache invalidation · scope cutting
You have three weeks to a launch. The packaging pipeline does not attach cache tags at publish time, so a withdrawal means enumerating every object: a two-hour title at four-second segments across ten video rungs, five audio and eight subtitle renditions is roughly 41,000 URLs, purged at every POP, partially failing. Adding tags is a packaging change you cannot land in three weeks. Describe shipping under a deadline with debt you named rather than hid: the compensating control, the bound on exposure, who agreed, and when it was repaid.
Approach
- The probe is whether debt gets a bound and an owner or only a mention. Start by separating what must be true at launch - no segment served for a content version outside an active window - from what is merely expensive, which is how you achieve it.
- Choose a compensating control that spends engineering time you have rather than pipeline time you do not. A manifest TTL shorter than the smallest rights-window granularity you actually have to honour bounds the exposure without touching packaging: if windows move on hour boundaries, a five-minute manifest TTL bounds it to five minutes. The cost is a higher origin manifest request rate, which you compute from concurrency and TTL before you promise anything.
- State the residual honestly. Segments already in edge caches keep serving for their own TTL, and a client already holding the old manifest keeps its segment list until it refetches - so the bound is manifest TTL plus client refresh behaviour, not the TTL alone, and offline copies are outside it entirely. A candidate who quotes only the first term has not read the failure through.
- Write the debt as an item with a size, a trigger and an owner: tags attached at publish time, three engineer-weeks, blocks any same-day withdrawal, due before the first title with a hard contractual pull date. Debt with a trigger gets repaid; debt with only a date slips.
- Say who agreed and what they were shown. A bounded legal exposure is not an engineering decision taken alone, and the answer that lands names the non-engineer who accepted the bound.
- Close on repayment and proof: what shipped, when, and the rehearsed purge on an asset nobody was watching - because the first real withdrawal is a poor time to discover the tag was attached to the wrong scope.
Follow-up
- A withdrawal lands the week after launch. Walk me through that hour.
- Your manifest TTL is five minutes. What did that do to origin manifest QPS at peak, and did you check it survives?
- What about copies already downloaded onto devices?