upskill / system design / read all, drill eight, own one
System design
Boxes are cheap. The interview is won at the tradeoff.
The method: read every answer once for breadth, whiteboard eight of them cold, and own one flagship end to end. The flagship is VoicerAI, at the bottom of this page: a real system with real numbers. At a YC startup, the system you actually built beats the one you memorized.
0 / 38
01The frame
Four moves, every time
Forty five minutes, one shape. Only the deep dive changes between questions. The algorithms and data structures underneath all of this live in the DSA track.
move 1
Requirements and scale
Functional first (what it does), then non-functional (latency, availability, consistency). Then do the envelope out loud: DAU, QPS with a 10x peak, storage per year, cache size. The numbers live in the estimation drill. Two minutes of arithmetic buys you the whole design.
move 2
High-level design
Draw the boxes, name the arrows, then walk one request through end to end. If you cannot narrate a single write and a single read across your own diagram, the diagram is decoration.
move 3
Deep dive the hard part
The interviewer picks one box, or you steer to your strongest. Every card below names its hard part so you know where the follow-up is coming from. Steering is allowed: "the interesting problem here is the fanout, want me to go deep on that?"
move 4
Wrap
Failure modes (what breaks first), bottlenecks (what saturates first), and what you would build first if this were week one. That last answer separates people who ship from people who diagram.
fundamentalsthe twelve moves
Load balancing
L4 spreads connections, L7 reads the request and can route on it. Round-robin by default, least-connections for uneven work, consistent hashing when servers hold session or cache state. Health checks make it your failover mechanism too.
Caching
Levels: browser, CDN, gateway, application, database. Cache-aside is the default pattern; invalidate with TTL plus explicit purge on write, and say the staleness window out loud. Stampede protection: a per-key lock or probabilistic early refresh, so one request rebuilds the value instead of ten thousand.
Sharding and partitioning
Hash sharding spreads load evenly but kills range queries; range sharding keeps them but invites hot spots. The shard key is the real decision: pick it so one entity's data lands together and no entity dominates. Resharding hurts, which is why consistent hashing exists.
Replication
Leader-follower: writes to one node, reads scale across copies. Async replication means followers lag, so a read after a write can travel back in time. Failover is the hard part: promote via consensus, and fence the old leader so you never get two.
CAP, honestly
During a partition you choose: refuse requests (consistency) or serve possibly stale data (availability). Not a slogan, a per-operation decision: a payment check picks C, a like counter picks A. Say which operations give up what.
Consistency levels
Strong: everyone sees every write instantly, paid for in latency and availability. Read-your-writes: you see your own updates, others can lag; this is what products usually need. Eventual: replicas converge, order unspecified. Most designs mix all three by endpoint.
Queues and backpressure
A queue decouples producer rate from consumer rate and absorbs spikes. Queue depth is your earliest warning signal: bound it, and when it fills either shed load or push back upstream. Unbounded queues turn overload into an out-of-memory crash later.
CDN and edge
Static assets always, with content-hashed immutable URLs so cache invalidation stops existing. Then dynamic reads that tolerate a short TTL. Egress and origin offload is usually the biggest single cost lever in media-heavy systems.
Database indexes
B-tree by default: equality and range in O(log n). Composite index column order matters: it serves prefixes only. A covering index answers from the index alone. Every index taxes every write, so index the queries you have, not the ones you fear.
Rate limiting algorithms
Token bucket: refill at rate r, allow bursts up to bucket size; the usual answer. Sliding window counter: smooths bursts, weights the previous window. Fixed window has the boundary bug: 2x the limit across the reset line. Return 429 with Retry-After.
Idempotency keys
Client sends a unique key per logical operation; the server stores key to result and replays the stored result on retry. This is how at-least-once delivery becomes exactly-once effect. Any endpoint that moves money or sends messages needs one.
Realtime delivery
Websockets: bidirectional, stateful, needs sticky routing; for chat and games. SSE: one-way server push over plain HTTP with auto-reconnect; for feeds and LLM token streams. Long polling is the fallback everywhere. Stateful connections are why realtime systems shard by session.
02Building blocks
The nine primitives
Every product question below decomposes into these. If you can whiteboard the key-value store and the message queue cold, you can derive half of everything else in the room.
Base62 of a counter over hashing: no collisions to retry, 7 chars covers 3.5 trillion URLs. Allocate counter ranges per node so the counter is not a single point.
301 vs 302: 301 caches in the browser and kills your analytics; 302 keeps every click on your servers. A business choice, say so.
Reads dominate about 100:1, so cache the slug lookup (cache-aside, LRU) and let the DB take misses only.
hard part
One viral slug dwarfing all other traffic: cache plus CDN absorbs it, and expired or deleted slugs must not be re-issued.
Token bucket when bursts are legitimate; sliding window counter when smoothness matters; fixed window is wrong by 2x at the boundary.
Counters in Redis with a Lua script so check-and-increment is atomic across gateway instances; in-process counters only if approximate is acceptable.
Fail open or fail closed when Redis dies: open for product endpoints, closed for auth and payments. Decide before the outage.
hard part
The accuracy vs synchronization tradeoff: exact global limits cost a network hop per request; most systems accept small overshoot.
Consistent hashing
testing: whether you know why mod N fails
components
hash ring, virtual nodes, membership service (gossip or ZooKeeper), client library or routing tier
key decisions
Servers and keys hash onto the same ring; a key belongs to the next server clockwise. Adding a node remaps about 1/N of keys; mod N remaps nearly all of them.
100 to 200 virtual nodes per server smooths the load imbalance of random placement and lets big machines take more of the ring.
Ring ownership: client-side (memcached style) vs every node can route (Dynamo style). The second survives dumb clients.
hard part
Hot keys still land on one node: the ring balances key count, not key traffic. Bounded-load variants and key replication answer that.
Quorum tuning: W+R greater than N gives read-your-writes; drop W for write speed, R for read speed, and name what you lose.
Conflict resolution: vector clocks surface siblings to the application; last-write-wins is simpler and silently drops data (Cassandra chose it anyway).
LSM over B-tree for write-heavy load: sequential writes now, compaction later, bloom filters to save the read path.
hard part
The repair machinery: hinted handoff for short outages, Merkle-tree anti-entropy for long ones, and tombstones that resurrect deletes if compaction outruns repair.
Distributed cache
testing: invalidation, stampedes, hot keys
components
client library, consistent hashing across shards, optional replicas, eviction policy, metrics on hit rate
key decisions
Cache-aside as the default; write-through when you need read-after-write; write-back is fastest and loses data on crash, so only for tolerable writes.
Stampede protection: per-key mutex or probabilistic early expiry, so an expiring hot key is rebuilt by one request, not a thundering herd.
TTL plus explicit invalidation on write, and an honest staleness window stated per data class.
hard part
The hot key: one celebrity key saturates one shard. Replicate the top-N keys or cache them client-local, and know your eviction policy under skew (LRU vs LFU).
Distributed message queue (Kafka-style)
testing: log vs queue, ordering scope, delivery guarantees
components
producers, brokers with partitioned append-only logs, replication with in-sync replicas, consumer groups with offsets, metadata (KRaft/ZooKeeper)
key decisions
Log with consumer-owned offsets (Kafka) vs broker ack-and-delete (RabbitMQ): the log gives replay and cheap fanout; the queue gives per-message routing.
Partition key sets ordering scope: order holds only within a partition, so key by the entity that needs it (user id, order id).
At-least-once plus idempotent consumers beats chasing exactly-once; Kafka's transactional exactly-once stops at its own boundaries.
hard part
Consumer group rebalances stalling consumption, and partition skew from a bad key turning one broker into the bottleneck.
job store, trigger service (indexed next-run scan or timing wheel), lease-based workers, retry policy, dead letter queue, status API
key decisions
Workers pull with a lease (visibility timeout): a crashed worker's job simply reappears when the lease expires. Push requires tracking worker health.
Exactly-once execution is a myth: promise at-least-once and make job payloads idempotent with an execution key.
WHERE next_run <= now with SKIP LOCKED is fine for millions of jobs; a hierarchical timing wheel when the scan becomes the bottleneck.
hard part
The thundering herd at popular cron minutes (everyone schedules on the hour): jitter, sharded scans, and per-tenant fairness.
Object storage (S3-style)
testing: metadata vs data planes, durability math
components
API gateway, metadata service (key to placement), data nodes with immutable chunks, erasure coding, background scrubber and repair
key decisions
Split metadata from bytes: the metadata DB, not the disks, is the real scaling problem (flat keyspace, list operations, billions of rows).
Erasure coding (say 8+4) over 3x replication: 1.5x storage overhead for eleven-nines durability, paid for in repair bandwidth and CPU.
Objects are immutable; overwrite creates a version. That one rule makes caching, replication, and repair all trivial.
hard part
Listing a bucket with billions of keys (prefix sharding of the metadata index) and multipart upload: parts, completion, and orphan cleanup.
03Products
The classics, one twist each
Each of these is a building block plus one twist. Name the twist in the first five minutes: the interviewer wrote the question around it.
Pastebin
testing: can you keep a simple problem simple
components
API, key generator, metadata DB, object store for paste bodies, cache, CDN for public pastes
key decisions
Blobs in object storage, metadata in the DB: do not stuff megabyte text bodies into database rows built for kilobytes.
Expiry is lazy: check on read, sweep in the background. No per-paste timers.
Same shape as the URL shortener, read-heavy: cache popular pastes, CDN the public ones.
hard part
Restraint, and the unglamorous real problem: abuse. Spam and leaked-secret takedowns are the operational load, not the reads.
Web crawler
testing: politeness, dedup at scale, frontier design
components
seed list, URL frontier (priority front queues, per-host back queues), DNS cache, fetcher pool, parser, URL and content dedup, storage
key decisions
Politeness by construction: one host maps to one back queue maps to one fetcher, so you cannot hammer a domain even by accident.
Dedup cheaply: bloom filter for seen URLs (accept rare false positives), SimHash for near-duplicate pages.
Recrawl by observed change rate per page, not on a uniform schedule.
hard part
Crawler traps: calendars and faceted search generate infinite URL spaces. Depth caps, URL normalization, and per-domain budgets.
Search autocomplete (typeahead)
testing: precompute vs query-time work
components
query log gathering, offline aggregation, trie builder with top-k cached per node, sharded serving layer, browser and edge caching
key decisions
Precompute top-k per prefix and store it at the trie node (or a flat prefix table): never rank at query time, the p99 budget is about 100 ms including network.
Daily offline rebuild plus a small realtime layer for trending queries, merged at serve time.
Shard by historical prefix load, not alphabetically: "s" and "x" are not the same size.
hard part
The update path: folding fresh counts into a precomputed structure without rebuild pauses, and filtering offensive suggestions before they trend.
Notification system
testing: multi-channel fanout, reliability without spam
websocket chat servers, session registry (user to server), message store (write-heavy wide-column), per-conversation sequence, presence, push notifications
key decisions
Long-lived websocket per client; a session registry makes send a lookup plus one hop between chat servers.
Order per conversation with a sequence number, never wall clocks: clocks lie, sequences do not.
Persist first, then push: offline users get a notification and sync on reconnect via a per-device cursor. Receipts are just messages.
hard part
Multi-device sync (each device is its own cursor) and group fanout, which is why group sizes are capped; end-to-end encryption fights every server-side feature.
Twitter timeline and search
testing: hybrid fanout plus a near-realtime index
components
tweet ingestion, fanout service, timeline caches, follower graph, realtime inverted index sharded by time and term, ranking, trends pipeline
key decisions
Hybrid fanout exactly as in the news feed: push for most, pull and merge for the celebrity tail.
Time-sharded search index: most queries want recent tweets, so the hot shards stay small and in memory; a tweet becomes searchable in seconds.
Trends are approximate: count-min sketch over a sliding window, not exact counters.
hard part
Doing both at once: the write path that feeds timelines and the index must not contend, so both hang off the same ingestion log.
Accept the original, return fast, process async: thumbnails and renditions come from a queue, never the upload request path.
Shard metadata by user id so a profile is one shard; Snowflake-style photo ids sort by time within the shard for free.
Immutable, content-addressed media URLs: infinitely cacheable, CDN does the serving.
hard part
Feed generation over a skewed follow graph, and storage economics: old photos need colder tiers without breaking those immutable URLs.
Dropbox / Google Drive
testing: sync protocol, dedup, conflict handling
components
client with local index and watcher, content-defined chunker (about 4 MB blocks), block store, metadata service (namespace, file to block list, versions), change notification via long poll
key decisions
Content-hash addressed blocks: dedup across users, delta sync (only changed blocks travel), and resumable transfers, all from one decision.
Metadata and blocks in different stores: small consistent relational data vs big immutable cheap bytes.
Never merge conflicts: write a conflicted copy and let the human decide. Silent last-writer-wins loses someone's work.
hard part
Sync correctness under offline edits, renames, and case-insensitive filesystems: the state machine of "what changed where" is the product.
YouTube
testing: transcoding pipeline, CDN economics
components
upload service, raw store, DAG transcoding pipeline over GOP-aligned segments, HLS/DASH packaging, CDN, metadata DB, view-count stream
key decisions
Split into segments, transcode in parallel across the resolution and codec ladder: hours of serial work becomes minutes.
Adaptive bitrate (HLS/DASH): the client switches rungs per network; you ship a ladder, not a file.
Zipfian popularity: CDN the head, keep the endless tail on cheap origin storage; view counts are approximate live, exact in batch.
hard part
Cost. Transcode compute and CDN egress dominate everything; per-title encoding settings and tiered storage are where the money is.
Protocol picks your latency tier: plain HLS (5 to 30 s) rides any CDN; LL-HLS lands around 2 to 5 s; WebRTC goes sub-second but needs stateful media servers and loses the CDN.
Transcode must keep up in real time: dedicated GPU pool, and degrade by dropping renditions, never the stream.
Chat is its own system: sharded pub/sub per channel with slow-mode rate limiting.
hard part
Start storms: a raid drops 100k viewers on a stream in seconds, so edge caches must fill from one origin fetch, not 100k.
Proximity service (Yelp, nearby friends)
testing: geo indexing tradeoffs
components
business write service (rare writes), location search service (heavy reads), geo index, cache, ranking
key decisions
Geohash vs quadtree: geohash is prefix search on an ordinary B-tree or Redis, fixed grid, easy to shard; a quadtree adapts to density but lives in memory and needs rebuilds. Geohash in a DB is the pragmatic answer.
Search the cell plus its 8 neighbors: two points can sit adjacent across a cell boundary, so one cell is never enough.
Nearby friends flips the physics: live locations mean websockets and pub/sub on updates, not a static index.
hard part
Precision choice under wild density variance: the right cell size for Manhattan is wrong for Montana.
Google Maps
testing: precomputation over graphs, tile serving
components
tile pipeline and CDN, geocoder, road graph, routing service, live traffic ingestion, ETA model, navigation sessions
key decisions
Tiles are precomputed and immutable at about 21 zoom levels, CDN-cached; vector tiles let the client restyle without new bytes.
Plain Dijkstra does not scale to a continent: contraction hierarchies (precomputed shortcuts over important nodes) make cross-country routes take milliseconds.
Traffic blends into edge weights from live probe speeds; reroute active sessions when the delta is significant.
hard part
Freshness of the precomputed routing structures: traffic changes the weights that the shortcuts were built on.
04Marketplaces and money
Where retries meet money
Money changes the physics. Delivery is at-least-once, effects must be exactly-once, and every answer in this section eventually becomes idempotency keys, ledgers, and reconciliation.
Uber
testing: live location at scale, dispatch consistency
components
rider and driver apps, location ingestion, in-memory geo index of drivers (geohash/S2 cells), matching service, trip state machine, surge pricing, ETA service
key decisions
Driver pings are ephemeral: update an in-memory cell-to-drivers index every few seconds, append the stream to Kafka for analytics, and keep durable writes off the hot path.
Match by ETA, not straight-line distance: the river in between matters.
Lock the driver with a short lease during the dispatch offer, so two riders cannot claim one car.
hard part
Exactly-one-driver-per-request without a global lock, and surge computed per cell from sliding-window supply and demand counters.
Food delivery (DoorDash)
testing: three-sided marketplace, state machines
components
order state machine, restaurant and menu service with caches, courier dispatch (geo matching), prep-time and ETA prediction, notifications, payments
key decisions
The order is a state machine on an event log (created, paid, accepted, cooking, picked up, delivered): three apps are three views of one machine.
Dispatch is timed to prep prediction: the courier should arrive as the food does, and batching orders trades their time against your margin.
Menu and hours caches need tight TTLs: a stale menu is a cooked refund.
hard part
ETA composition (prep plus travel plus parking plus handoff), each term with its own error bars, promised to the customer as one number.
Payment system
testing: correctness under retries, reconciliation
Idempotency key on every mutation: the server stores key to result and replays it, so a client retry can never double-charge.
Double-entry, append-only ledger: every movement is two entries summing to zero, balances are a fold over the log, nothing is updated in place.
No distributed transaction with the PSP: a local state machine advanced by webhooks and polling, then reconciliation against the PSP's records to catch drift.
hard part
Exactly-once effect built entirely out of at-least-once parts: retries, idempotency, and reconciliation are the whole design.
Digital wallet
testing: cross-shard transfer atomicity, hot accounts
Cross-shard transfer: 2PC is atomic but blocks on coordinator failure; sagas trade atomicity for availability via compensating transactions; event sourcing with deterministic replay makes every balance reproducible and auditable.
Balances are views over the event log, rebuildable from history: the audit trail is the data model.
Hot accounts (a merchant credited thousands of times a second): shard into sub-accounts and merge periodically.
hard part
Atomic transfer across shards at high throughput, and proving to an auditor that the books balance at any past instant.
Stock exchange
testing: determinism, microsecond tails, fairness
components
gateway with risk checks, sequencer, in-memory matching engine (order book per symbol, price-time priority), market data publisher, execution reports, replicated event log
key decisions
Single-threaded matching engine per symbol: no locks, deterministic, microseconds. Scale across symbols, never inside one book.
A sequencer totally orders all events; replicas replay the same sequence into the same state: failover is state machine replication, not backup restore.
Fairness is architecture: price-time priority in the book, market data fanned out without favorites.
hard part
Failover that provably loses and reorders nothing, and p99.9 in microseconds, which means no GC and no syscalls on the hot path.
Ticketmaster
testing: contention on scarce inventory
components
event and seat inventory DB, virtual waiting room, reservation service with TTL seat holds, payment, confirmation
key decisions
Seat hold is a conditional update: UPDATE ... WHERE status = 'free' with a reserved-until expiry; optimistic beats pessimistic locking when most claims fail anyway.
The waiting room is the design: admit users in batches so 10 million people compete at a concurrency the database survives.
Holds expire server-side via a sweep or delay queue; the client timer is a lie you rendered.
hard part
Not overselling seat 14B under thousands of simultaneous claims while the site stays up for browsing.
Hotel reservation
testing: inventory modeling, overbooking as policy
components
inventory service (rooms per type per date), search, reservation service, payment, admin tooling
key decisions
Book room types, not rooms: inventory is a counter per (hotel, type, date); the physical room is assigned at check-in. This collapses the whole problem.
Concurrency by conditional decrement (WHERE available >= 1) or a version column; a multi-night stay must decrement every date atomically, all or none.
Overbooking is a business rule, say 110%, sitting inside the same conditional: cancellations are expected inventory.
hard part
Multi-night atomicity across date rows, and keeping cached availability honest enough that search rarely sells what booking cannot deliver.
Ad click event aggregation
testing: streaming correctness at billing grade
components
click stream (Kafka), stream aggregator (Flink), minute windows keyed by ad id, watermarks, aggregate store, batch recompute from the raw log
key decisions
Stream for live numbers, batch recompute from the immutable raw log for the invoice: clicks are money, so keep a path to the exact answer.
Event time with watermarks, never processing time: late clicks land in the right minute or emit a correction record.
Exactly-once aggregation via checkpointing plus a transactional or idempotent sink.
hard part
Late and duplicate events at billing accuracy, with fraud filtering upstream deciding which clicks were ever real.
05Infra and realtime
The plumbing, plus the AI-era two
What runs underneath the products, and the two questions YC startups actually ask now: the LLM gateway and the RAG pipeline. Both connect straight into the flagship below.
A real TSDB: Gorilla-style delta-of-delta timestamps and XOR-compressed floats get a point near 1.4 bytes, with the recent window in memory because most queries are "last hour".
Pull vs push: scraping (Prometheus) gives discovery and up/down for free; push suits ephemeral jobs behind NAT.
Retention tiers: raw for weeks, rollups for years; alerts evaluate over the stream with a for-duration to kill flapping.
hard part
Cardinality explosions: one user-id label mints millions of series and the index, not the data, kills you.
Real-time leaderboard
testing: knowing when a sorted set is the whole answer
Redis ZSET is the design: ZINCRBY to score, ZREVRANK for rank, O(log n) each; tens of millions of players fit one node's memory. SQL ORDER BY cannot compete.
Redis is a view, not the truth: the score events live in a log or DB, so a lost node is a replay, not a loss.
Past one node: shard by score range (rank = local rank + counts above) or scatter-gather top-k across user shards.
hard part
Exact global rank for a mid-pack player across shards, and tie-breaking rules that stay stable under concurrent updates.
Collaborative editor (Google Docs)
testing: convergence, OT vs CRDT
components
client editors, websocket sync service, per-document session with an ordered op log, transform or merge engine, snapshots plus compaction, presence and cursors
key decisions
Operational transform vs CRDT: OT transforms concurrent ops against each other and needs a central sequencer (Google Docs); CRDTs commute by construction and work offline and peer-to-peer at a metadata cost (Yjs, Figma-adjacent). With a central server, OT stays simpler.
Document = op log + periodic snapshots: a joining client loads the snapshot and replays the tail.
Sticky per-document sessions: every op for a doc flows through one node for ordering; shard by document id.
hard part
Proving convergence for every concurrent op pair in OT, or taming CRDT tombstone and metadata growth over a document's lifetime.
SMTP ingress with spam and virus pipeline, immutable message store, label and thread metadata, per-user search index, IMAP/HTTP API, outbound MTA
key decisions
Messages are immutable: folders and labels are metadata pointers, so "move to archive" is a metadata write; attachments dedup by content hash.
Search index per user, built at delivery time: privacy isolation and a small index beat one global index for private corpora.
Outbound is reputation management: IP warm-up, SPF/DKIM/DMARC, bounce processing. Deliverability, not code, is the moat.
hard part
Spam classification at ingress, and IMAP flag sync semantics across devices that disagree about state.
Video conferencing (Zoom)
testing: realtime media topology
components
signaling service (websocket), SFU media servers, STUN/TURN, simulcast layers, regional media routing, recording pipeline
key decisions
SFU over MCU over mesh: mesh uploads O(n) streams per client; an MCU decodes and mixes (CPU-heavy, adds latency); an SFU just forwards selected streams. SFU is the standard for 3+ people.
Simulcast: each client uploads 2 or 3 quality layers and the SFU picks per receiver, so no transcoding sits on the hot path.
UDP/RTP with jitter buffers, FEC and loss concealment: retransmission arrives too late for live audio; latency beats completeness.
hard part
Bandwidth estimation and mid-call adaptation without visible quality thrash; cascade SFUs across regions for big meetings.
API gateway with SSE streaming, model router, token budgeter, priority queue, prompt caches, provider adapters with circuit breakers, usage metering
key decisions
Rate limit in tokens, not requests: one request can cost 100x another. Admit on an estimate, settle on actuals against per-key budgets.
Stream end to end: time to first token is the UX metric, so SSE passthrough with no buffering middlebox.
Cache in layers: exact-match for repeated prompts, prefix/KV-cache reuse for shared system prompts, semantic caching only with an honest staleness story.
Route cheap-first with escalation, health-based failover across providers, and retry only if no token has been streamed yet.
hard part
Fairness under GPU scarcity: multi-second requests mean queueing theory and per-tenant isolation, not just a limiter.
RAG pipeline
testing: retrieval quality as a systems problem
components
ingest and chunker, embedding workers, vector index (HNSW) plus keyword index (BM25), hybrid retriever, cross-encoder reranker, prompt assembly with citations, eval harness
key decisions
Chunk by document structure (headings, paragraphs) with overlap, a few hundred tokens each: chunk boundaries decide answer quality more than the model does.
Hybrid retrieval merged by reciprocal rank fusion: embeddings miss exact identifiers, keywords miss paraphrase; you need both.
Retrieve wide, rerank narrow: cheap recall to top 50, cross-encoder precision to top 5, and carry source ids through so every claim cites a chunk.
hard part
Freshness and eval: upsert with tombstones on doc change, a full reindex on embedding-model upgrade (version your embeddings), and continuous retrieval metrics or quality silently rots.
06The flagship
VoicerAI: the one I own
Read all, drill eight, own one: this is the one. I built voicer-ai, a real-time voice agent, and this is how I present it as a system design. The full build story, live demo included, is at anishfyi.com/voicer/.
mark the flagship read
requirementsWhat I set out to build
A real-time voice conversation with a machine: sub-second perceived response, the ability to interrupt it mid-sentence and have it stop (barge-in), zero per-minute cost, and nothing leaving the machine. No API keys, no cloud bill, every stage a free local model. About 750 lines of Python, no framework, and every stage swappable between a local and a cloud provider behind one interface. The non-functional requirement that dominates everything else is latency: humans read anything past 1.5 seconds of silence as broken.
architectureThe cascade
mic30 ms frames
VADsilero / energy
ASRfaster-whisper
LLMollama, streamed
chunkersentence bounds
TTSpiper
speakerinterruptible
the VAD keeps running during playback. that single loop is barge-in
I chose a cascaded pipeline over a speech-to-speech model deliberately. The cascade gives me text in the middle: every stage is inspectable, every provider is swappable, and the transcript is a free audit trail for logging, evals, and deterministic tool calls. Speech-to-speech models collapse the stages for latency and keep prosody, but tools, evals and audits all get harder. For an agent that does things, I want the text seam. For a companion where naturalness is the product, I would choose the other way, and I can argue both sides.
latencyThe budget, term by term
Stage
Local models
Cloud APIs
Who controls it
VAD endpoint (hangover)
300-700 ms
300-700 ms
you. a constant you chose
ASR (5 s utterance)
300-800 ms
200-500 ms
model + streaming
LLM to first sentence
n/a
300-700 ms
model + prompt length
TTS first audio buffer
100-400 ms
150-400 ms
provider + streaming
Total to first audio
0.9-2.6 s
0.95-2.3 s
Under 300 ms feels magical, 500 to 800 ms feels fine, past 1.5 s the agent reads as slow, past 3 s people repeat themselves or hang up. The number that should bother you: the largest single line item is the hangover, the 600 ms of silence (20 frames at 30 ms) I wait before declaring the turn over, and it is a constant I chose. No model upgrade touches it. That is why the frontier is semantic endpointing and turn prediction, not better silence detection. The back-of-envelope habit is the same one as the estimation drill: know your budget before you defend your boxes.
controlThe state machine, and the one insight
LISTENING --speech end--> THINKING --first sentence--> SPEAKING
^ |
+--------- barge-in, or all sentences spoken ----------+
Three states, one loop, and the thing that is not in the diagram is the whole trick: the mic thread and the VAD never stop, even while the agent is speaking. Barge-in is not a feature bolted on top; it is just the VAD staying on during playback, pushing an event when it hears you, and the main loop stopping the player. If the mic muted during SPEAKING, the transition out of it could never fire. On interruption there are four decisions: stop playback immediately, discard the unspoken sentences, record in history only what was actually said aloud (or the model later references things you never heard), and treat any in-flight tool call as the dangerous case: finish, cancel, or make it idempotent.
speedSentence-level pipelining
# the trick that makes it feel alive
without: TTFA = ASR + full LLM response + TTS(all of it)
with: TTFA = ASR + LLM first sentence + TTS(first sentence)
I chunk the LLM's token stream into sentences and hand sentence one to TTS while the model is still writing sentence three. For a three-sentence answer that is roughly a 3x cut in perceived latency, with no better model, no faster hardware, no spend. The catch is boundary detection in a live stream: "Dr." looks like the end of a sentence, and my chunker once stalled on exactly that, silently reverting the whole system to full-response latency while every test stayed green. A latency bug that produces correct output is invisible to a correctness suite.
failureHow it dies in practice
False endpointing on thinking pauses. Hangover too short and the agent cuts you off mid-thought, then cuts off your correction too. Trust dies in one turn. Too long and every turn pays dead air.
ASR fed silence hallucinates. Endpointing mistakes hand Whisper near-silent audio and it invents text; faster-whisper's own VAD filter runs as a second line of defense on exactly this.
TTS latency spikes hide between sentences. Playback blocks the loop, so a slow synthesis call shows up as a mid-reply gap, not as TTFA. If you only measure TTFA, you will not see it.
Echo makes the agent interrupt itself. Without acoustic echo cancellation the mic hears the speaker, VAD fires, and the agent stops for its own voice. The repo assumes headphones and says so; production means AEC.
scaleFrom one laptop to a fleet
The single-process design maps cleanly onto a service. Websocket ingress with session affinity: a conversation is stateful (VAD state, dialogue history, playback position), so a session pins to one stateful session worker that owns its state machine. The real decision is model serving: a per-session monolith (all stages colocated with the session) is simple and keeps hops off the critical path but wastes accelerators, versus shared per-stage pools: an ASR pool, an LLM pool with continuous batching, a TTS pool. Pools win on utilization and independent scaling, and cost you network hops inside the latency budget. My answer: colocate at small scale, then pool the LLM first, because it is the most expensive stage and batches best. Observability is per-stage latency histograms, p50 and p99, with TTFA measured from the user's last word, not the endpoint decision: the second zero flatters you by the entire hangover. Then barge-in success rate and false barge-in rate, because those are the product.
pitchSixty seconds, cold
voicer-ai is a real-time voice agent I wrote to own the whole stack instead of one API call: mic into a voice activity detector, then speech recognition, then a streaming language model, then text to speech. About 750 lines of Python, no framework, every stage swappable between a local model and a cloud API, total running cost zero. Two pieces make it feel alive. Barge-in: the mic thread never stops, so when you interrupt, it stops. And sentence pipelining: I chunk the token stream into sentences and synthesize the first while the model writes the third, which turns time to first audio into recognition plus one sentence instead of recognition plus the whole response. The honest limitation: utterance-level recognition, not streaming, and no echo cancellation, so barge-in wants headphones. Both are written down in the repo, because naming your own gaps is cheaper than being caught by them.
github.com/anishfyi/voicer-ai ยท the deep dive: anishfyi.com/voicer/