Design a real-time voice assistant
The interviewer asks
“Design a voice assistant that users can talk to naturally inside a mobile app (and over a phone call), for example to track orders or rebook a ticket. It should reply within about a second, let users interrupt it mid-sentence, and support Hindi and English.”
What a strong answer looks like
A strong answer starts from the latency budget, because voice feels broken above roughly a second of silence. The classic pipeline is streaming: client audio over WebRTC to a regional media edge, voice activity detection and turn detection, streaming speech-to-text, an LLM that streams tokens, and streaming text-to-speech that starts speaking after the first sentence. Every stage must stream and overlap; nothing waits for the previous stage to finish. A stateful per-session orchestrator owns the conversation, handles barge-in (stop audio, cancel the LLM, and record only what the user actually heard), and calls tools through a narrow, confirmed API. Strong candidates compare this cascaded pipeline with speech-to-speech realtime models, place sessions in the region nearest the user, plan GPU capacity for ASR and TTS by concurrent streams, and handle Indian realities: Hinglish code-switching, 8 kHz phone audio and lossy mobile networks.
Concepts to mention
1 Requirements
Functional
- Hands-free conversation from the app (WebRTC) and from a phone number (PSTN via SIP trunk)
- Speech recognition and replies in English, Hindi and mixed Hinglish; reply in the user's language
- Barge-in: the user can interrupt; the assistant stops within ~200 ms and listens
- Tool use: look up orders, cancel or rebook with explicit spoken confirmation for actions
- Hand off to a human agent with the transcript and a summary
- Transcripts and call summaries stored for the user and for quality review (with consent)
Non-functional
- Voice-to-voice latency (user stops speaking → first assistant audio): p50 < 800 ms, p95 < 1.5 s
- Stop speaking within 200-300 ms of real user speech during assistant playback
- Peak 50k concurrent sessions (evening peak, sale days); average session 3 minutes
- Works on lossy 4G with 2-5% packet loss and 100-300 ms RTT jitter
- Session availability 99.9%; a failed component degrades (e.g. text fallback), not a dead call
- Audio and transcripts of Indian users stored in India; recordings only with consent
Out of scope (say it out loud)
- Wake-word detection on a smart speaker
- Training ASR, LLM or TTS models
- Voice biometrics / speaker verification
- Full contact-centre routing and agent desktop
2 Estimates
3 API
| Endpoint | What it does |
|---|---|
POST/v1/voice/sessions | Creates a session: picks the nearest region and returns {session_id, media_url, short-lived token, ICE servers}. |
WebRTCmedia: Opus audio both ways + data channel | Audio in/out; the data channel carries events: partial transcript, assistant text, tool status, interruption, end. |
SIPsip:assistant@trunk (telephony) | Phone calls arrive from a SIP trunk provider; the media gateway converts 8 kHz G.711 to the internal pipeline. |
WS/v1/voice/sessions/{id}/ws | Fallback transport over TCP/443 when UDP is blocked on corporate or hotel Wi-Fi; higher latency on lossy links. |
POST/v1/voice/sessions/{id}/handoff | Transfers to a human queue with transcript and summary. |
GET/v1/voice/sessions/{id}/transcript | Transcript with speaker turns and timestamps, after the call. |
4 Data model
session_stateIn-memory in the orchestrator + Redis snapshotsession_iduser_idregionlanguageturns[] (role, text, heard_text, ts)pending_tool_callplayback_cursorcreated_atHot state lives in the process that owns the session (sticky). A snapshot after every turn to Redis (TTL 1 h) lets another orchestrator resume if the process dies.
sessionsPostgressession_id (PK)user_idchannel (app|phone)regionstarted_atended_atoutcome (resolved|handoff|dropped)consent_recordinglatency_p50_msOne row per session for history, analytics joins and support lookups.
transcriptsS3 (Mumbai region) + search indexsession_id/transcript.json: turns with timestampsASR confidencetool calls; optional audio.opusLarge and append-once. Lifecycle: transcripts 180 days, audio 30 days unless flagged for quality review.
tool_auditPostgrescall_idsession_idtoolargs (redacted)confirmed_by_user (bool)result_statusidempotency_keytsEvery action the assistant took on the user's behalf, with proof of confirmation.
voice_eventsKafka → ClickHousesession_idevent (turn_start, endpoint, asr_final, llm_first_token, tts_first_audio, barge_in)ts_msStage timings per turn; this is how you measure and debug the latency budget.
5 Architecture
Hover a box to highlight its connections.
6 Request flows
One conversational turn
- 1The app calls POST /sessions; the session service picks the Mumbai or Hyderabad region by latency and returns a media token; the client connects with WebRTC (Opus, echo cancellation and noise suppression on device).
- 2The media edge forwards 20 ms audio frames to the orchestrator that owns this session; the orchestrator runs a VAD and streams speech frames to ASR.
- 3ASR returns partial transcripts every ~100 ms; a turn-detection model looks at audio silence plus the words ('my order number is...' is not finished) and declares end of turn after ~250 ms of silence when the sentence looks complete.
- 4The orchestrator sends the final transcript with conversation history to the LLM through the AI gateway, using a short system prompt that asks for one or two spoken sentences.
- 5As tokens stream back, a sentence splitter sends the first complete clause to TTS immediately; TTS streams audio chunks of ~40 ms back through the media edge.
- 6The first audio reaches the user ~700-800 ms after they stopped speaking; the rest of the reply is synthesised while the first sentence is playing.
- 7The orchestrator records the turn, stage timings and the text actually played, snapshots state to Redis, and emits events to Kafka.
User interrupts (barge-in)
- 1While TTS audio is playing, the VAD on the incoming stream detects speech energy that is not the assistant's own echo (device echo cancellation removes most of it).
- 2If speech lasts more than ~150-200 ms (to ignore coughs and 'hmm'), the orchestrator sends a stop event: the client flushes its playout buffer and the TTS stream is cancelled.
- 3The in-flight LLM stream is cancelled so no more tokens are paid for.
- 4History is truncated to what the user actually heard, using the playback cursor reported by the client, and the assistant turn is marked as interrupted.
- 5The new user speech, already streaming to ASR, becomes the next turn.
Spoken action with confirmation
- 1User: 'Cancel my order from yesterday.' The LLM returns a tool call find_orders(date=yesterday).
- 2The orchestrator calls the read-only tool directly and the LLM replies 'Your order of 2 kg atta, ₹180, placed at 7 pm. Should I cancel it?'
- 3On a clear 'yes' (checked by an intent classifier, not by the LLM alone), the orchestrator calls cancel_order with an idempotency key of session_id + turn number.
- 4The tool API checks the user's permissions and order state, cancels, writes the audit row with confirmed_by_user = true, and the assistant reads back the result.
7 Deep dives
Hitting the latency budget: stream and overlap everything
If each stage waits for the previous one, you get ASR 1 s + LLM 2 s + TTS 1 s = 4 s of silence, which feels broken. The fix is streaming at every boundary. ASR streams partial results as the user speaks, so the final transcript is ready ~50-100 ms after they stop. The LLM streams tokens, and a sentence splitter hands the first clause to TTS as soon as it ends (at a comma or full stop, with a minimum of ~5 words so speech sounds natural). TTS streams audio chunks so the first sound plays in ~100-150 ms. Spoken replies are prompted to be short, which also reduces time to the first sentence. The biggest remaining item is endpointing: waiting 800 ms of silence to be sure the user finished is safe but slow, and 200 ms cuts people off. A turn-detection model that combines silence with the meaning of the words lets you end turns at ~250 ms when the sentence is complete and wait longer after 'my number is 98...'. Other wins: keep the LLM prompt prefix stable so provider prompt caching cuts time to first token; colocate orchestrator, ASR and TTS in one region to keep internal hops under 5 ms; speculative LLM calls on a stable partial transcript, discarded if the final differs. Measure each stage per turn, because the p95 is usually dominated by one stage.
Barge-in and the 'what did the user hear' problem
Natural conversation means the user talks over the assistant. Three things must work. First, detection: the device microphone also hears the assistant's own voice, so echo cancellation on the device (WebRTC's built-in AEC) is essential, and the server VAD needs a minimum speech duration (~150-200 ms) so a cough or a backchannel 'haan' does not stop the reply. Second, stopping fast: the client holds a small playout buffer and must flush it on a stop event; the server cancels the TTS stream and the LLM stream so you stop paying for tokens no one will hear. Target under ~300 ms from the user starting to talk to silence. Third, state correctness: the LLM generated a full paragraph, but the user heard only the first sentence. If you store the full paragraph as the assistant's turn, the next reply may refer to things the user never heard. The client reports a playback cursor (audio milliseconds played), the orchestrator maps it back to text via TTS word timestamps, and stores only the heard part. This detail is what separates a demo from a production voice agent.
Cascaded pipeline vs speech-to-speech models
A cascaded pipeline (ASR → LLM → TTS) is modular: you can pick the best Hindi ASR, the cheapest capable LLM and a natural Indian-English TTS voice, inspect text at every step, apply text-based safety and PII redaction, and log a readable transcript. Its costs are the extra latency at each boundary and loss of tone: the LLM never hears that the user sounds angry. Speech-to-speech realtime models take audio in and produce audio out in one model, often reaching lower latency and more natural turn-taking, and they understand prosody. Their downsides today: less control over each step, harder evaluation and safety filtering, fewer choices of voice and language quality for Indian languages, vendor lock-in, and usually a higher price per minute. A strong answer designs the orchestrator so the 'brain' is pluggable: the same session, tool, confirmation and audit layers work whether the middle is a cascade or one realtime model. Many teams use a cascade for regulated flows (banking, where every word must be logged and checked) and realtime models for open-ended assistants.
Stateful sessions, regions and Indian networks
A voice session is stateful and latency-sensitive, so it should be owned by one orchestrator process for its whole life, close to the user. The session service picks a region by measured latency, and the media edge and orchestrator are pinned for the session; load balancing happens only at session start, by current concurrent sessions per node, not by request. Use WebRTC over UDP: Opus with forward error correction handles 2-5% packet loss, and the jitter buffer smooths uneven arrival, whereas a WebSocket over TCP stalls on every lost packet (head-of-line blocking). Keep a TCP/443 fallback for networks that block UDP. Phone calls arrive as 8 kHz narrowband audio, so the ASR needs a telephony-tuned model. For Indian users, ASR must handle code-switching within a sentence ('mera order kab aayega, it's been 3 days'); choose a model trained on Hinglish rather than running language detection per utterance. Numbers, names and addresses are the main error source, so confirm them back, and let users type or tap when ASR confidence is low.
8 Trade-offs
WebRTC vs WebSocket for audio
WebRTC gives UDP transport, Opus with packet-loss concealment, jitter buffers, echo cancellation and congestion control, built into browsers and mobile SDKs. It needs TURN servers and more operational work. WebSockets are simple but run over TCP, so one lost packet delays all following audio. Use WebRTC by default with a WebSocket fallback.
Short endpoint timeout vs long
A 200 ms silence timeout feels snappy but cuts users off mid-thought, especially when reading out numbers. A 900 ms timeout is safe but feels slow. A semantic turn detector plus a context-dependent timeout (longer after a question that asks for an order number) gives both.
Big LLM vs small LLM for voice
Bigger models reason better but have higher time to first token. Voice turns are mostly short and tool-driven, so a fast mid-size model with good tool calling usually wins; escalate to a larger model only for complex turns, accepting a filler phrase ('let me check that') to cover the delay.
9 Bottlenecks
GPU capacity for ASR/TTS at sale-day peaks
Concurrent streams, not requests per second, size the fleet. At 10× sessions, pre-scale before announced sales, use server-side VAD so silent audio never reaches ASR, cap concurrent sessions per region with a polite 'please try in a minute' or a text chat fallback, and spill over to a second region if latency allows.
LLM rate limits
3,600 calls/s can hit provider limits. Use the AI gateway with multiple providers and per-feature token budgets, keep prompts short and prefix-cached, and route simple intents (order status) to a deterministic flow that skips the LLM entirely.
10 Failure handling
Orchestrator process dies mid-call
The media edge detects the lost connection and asks the session service for a new owner; the new orchestrator loads the Redis snapshot from the last completed turn and says 'Sorry, I missed that, could you repeat?'. At most one turn is lost; tool calls are safe to retry because they carry idempotency keys.
TTS or ASR provider degraded
Health checks on time-to-first-audio trigger failover to a secondary engine (possibly a less natural voice). If ASR fails entirely, the app switches to a text chat with the same session state; on a phone call, the call is transferred to a human agent queue.
LLM slow (p99 spikes to 5 s)
After ~1 s with no first sentence, the orchestrator plays a short filler ('One moment, checking your order') from pre-recorded audio so the line is never silent, and the gateway hedges the request to a backup model after a deadline.
11 What impresses
- Opens with a millisecond budget per stage and names endpointing as the largest item
- Explains why the stored assistant turn must be truncated to what the user actually heard
- Chooses WebRTC with a reason (UDP, FEC, jitter buffer, AEC), not just 'use WebSockets'
- Makes the model layer pluggable between cascade and realtime speech-to-speech
- Sizes GPUs by concurrent streams using Little's law
- Handles Indian network and language realities without being asked
12 Mistakes
- Waiting for complete ASR, complete LLM answer and complete TTS before playing anything
- Ignoring echo, so the assistant interrupts itself
- Stateless round-robin load balancing for every audio frame
- Letting the LLM execute irreversible actions without confirmation
- Not measuring per-stage latency, so the p95 cannot be debugged
- Using a fixed long silence timeout and calling it done
13 Follow-ups
Lessons this design uses
Saved on this device. Create a free account to keep it on every device.
Practise this with Sam
A real mock interview, out loud, with an honest scorecard. Free to start.