Vibe Engines
YouTube
AI System Design

Design a Speech-to-Text Service

Learn AI system design by building a speech-to-text / ASR service like Whisper-at-scale, Deepgram or Google Speech step by step.

The numbers to beatstreamingpersistent conn, partialsbatchasync job + callbackshared fleetone model, two paths

The whole design, in writing

Learn AI system design by building a speech-to-text / ASR service like Whisper-at-scale, Deepgram or Google Speech step by step. An interactive guide covering batch vs streaming transcription, the ASR inference pipeline, chunking and voice activity detection, a batched autoscaling GPU fleet, post-processing (punctuation, diarization, timestamps), and the unhappy paths (real-time latency budget, accuracy vs chunk size, noisy audio, cost).

Every step of the build above, written out: the problem each piece solves, the option that was taken and the ones that were not, the numbers, and how it fails in production.

The big idea

What makes speech-to-text hard?

Turning audio into text splits into two very different products. Batch: transcribe an uploaded hour-long recording accurately (podcasts, meetings) — accuracy matters, latency doesn't. Streaming: caption a live conversation as it's spoken, showing words within a fraction of a second — latency is everything. Same model, opposite constraints, and both run heavy GPU inference.

Audio Sourcemic · file
New in this step: Audio Source.

A speech-to-text (ASR) service serves both modes over a GPU model. The shape: a gateway that splits streaming from batch, a batched GPU worker fleet running the model, chunking/VAD to feed it, and post-processing (punctuation, timestamps, diarization) to make raw tokens useful. The hard parts are the streaming latency budget and cost.

What the new pieces do

Audio Sourceclient
A live microphone stream (real-time captions) or an uploaded recording (batch transcription). Two very different modes.

Step 1 · Two modes, one model

Batch vs streaming

Before any architecture, recognize that "transcribe audio" is really two problems with opposite priorities. What are they, and why can't one design serve both naively?

Audio Sourcemic · file
The system as it stands at this step.

Why can't a single naive "upload → transcribe → return" design serve all speech-to-text?

  1. Batch can wait and see the whole recording for max accuracy; streaming must emit words within a fraction of a second before the utterance is even finished. These opposite priorities need different handling (async job vs persistent low-latency connection with partial decoding), even if they share the model.

  2. File size is manageable. The real split is latency: real-time captioning can't wait for a whole-file transcription, so it needs incremental, low-latency processing that a simple upload-and-return can't provide.

  3. The same model can serve both; the difference is how you feed it and return results — full-file async for batch, chunked low-latency streaming for real-time.

Two modes, opposite constraints. Batch can wait and use the whole recording for maximum accuracy — an async job. Streaming must emit words as they're spoken, within a fraction of a second, before the sentence is even done — a persistent connection with incremental, low-latency decoding. They share the model but need different plumbing, so the design splits them at the front door.

Why this piece earns its place

The split is easy to agree with and hard to hold, because the two modes are also measured differently and most teams only measure one. A batch transcript is scored against a reference after the fact, on a finished file. Streaming has a second axis batch does not have at all: how long a word takes to appear after it was spoken, and how much a partial changes before it settles. A streaming service can produce an excellent final transcript and still feel broken — flickering and rewriting itself under the reader — and a word-error number will never show it. The split also changes the API contract. Batch returns one immutable result; streaming returns a sequence of revisions, so every consumer downstream has to be able to unwrite text it has already displayed. A caption renderer or a log sink that only appends will duplicate and corrupt the transcript, and that is a client bug you will be blamed for. If the product only ever ingests recordings — a meeting archive with no live view — skip the streaming plumbing entirely. It is the expensive half.

Step 2 · The front door

A gateway that splits the paths

Given two modes, what does the entry point do the moment audio (live or uploaded) arrives?

Audio Sourcemic · fileGatewaystream · batch
New in this step: Gateway.

A gateway routes by mode. For streaming, it holds a persistent connection (WebSocket/gRPC) and pipes audio frames toward the workers, returning partial transcripts live. For batch, it accepts the file, enqueues an async job, and returns a job id to poll or a callback URL. Both feed the same GPU model fleet, but through different flow-control: real-time streaming vs buffered async jobs.

Why this piece earns its place

A stateless endpoint can hand any request to whichever worker is free. A streaming connection cannot: the decoder carries context from one chunk to the next, so a call is pinned to the worker it started on for its whole life. The gateway is what holds that call together, and the settings it holds — sample rate and encoding, a language hint, the vocabulary for this conversation — are agreed once at connection open and cannot be changed halfway through a sentence. The queue and the transcript store keep state too, but theirs survives a restart; this state is live, and it dies with the socket. Two things fall out of that. Clients drop, especially mobile ones, so the gateway needs a resume token and a defined behaviour for the audio that arrived while nobody was listening, or the caller silently loses the tail of a sentence and never learns it happened. And the connection is where you decide what a partial means: whether a revision may rewrite anything already sent, or only the last unsettled segment. Publish that rule, because callers will build against whichever one you happen to ship.

  • streamingpersistent conn, partials
  • batchasync job + callback
  • shared fleetone model, two paths

What the new pieces do

Gatewaygateway
Splits traffic: a persistent connection for streaming audio, or an async upload endpoint for batch files.

Step 3 · Buffer the batch

Async jobs for uploads

Uploaded recordings are heavy (an hour of audio is minutes of GPU work) and arrive in unpredictable bursts. How do you handle them without dropping jobs or starving the latency-sensitive streaming path?

Gatewaystream · batchBatch Queueuploaded filesASR Workeraudio → tokens
New in this step: Batch Queue, ASR Worker.

Send batch uploads through a job queue to the GPU workers, exactly like any heavy async job: enqueue, transcribe at the fleet's sustainable rate, deliver via poll or callback. Crucially, keep batch and streaming on separate paths/queues (or reserved capacity) so a flood of uploaded files can't consume the GPUs that real-time captioning depends on. Batch absorbs bursts as backlog; streaming keeps its low-latency lane.

Why this piece earns its place

A transcription job is long, and long jobs make the queue's delivery guarantees matter. At-least-once means a worker that dies near the end of an hour of audio redoes all of it unless the job checkpoints per segment, and that is GPU time bought twice for one transcript. It also means the completion can fire more than once, which costs no compute at all but arrives as a duplicate callback — and as a second charge, if billing meters completion events rather than audio processed. So jobs want an idempotency key, per-segment checkpoints, and a callback the receiver can safely get twice. Queue depth is the signal to scale on, but the unit is not jobs: one job is a voicemail and the next is a three-hour lecture, so count the backlog in seconds of audio and the same number becomes an honest ETA and a measure of the work rather than of the paperwork. One FIFO queue also stops being enough quickly: a bulk import from a single customer will sit in front of everybody else's short files for hours. Split by expected duration, or schedule per tenant. If uploads are small and infrequent, none of this earns its keep — transcribe synchronously and return the text.

  • batch → queueabsorb bursts
  • isolatestreaming stays low-latency
  • poll / callbackdeliver batch results

What the new pieces do

Batch Queuebus
Holds batch transcription jobs so uploaded recordings drain at GPU pace, isolated from the live streaming path.
ASR Workerservice
Turns audio features (a mel spectrogram) into text tokens on a GPU — batched with other requests for throughput.

Step 4 · Inside the model

The ASR inference pipeline

A worker has audio to transcribe — what does the model actually do to turn a waveform into words?

ASR Workeraudio → tokensASR Modelweights
New in this step: ASR Model.

Convert the raw waveform into audio features — typically a mel spectrogram (a time-frequency representation) — then feed it to the ASR model (a Whisper-style encoder-decoder or transducer), which produces a sequence of text tokens, decoded (often with beam search and an optional language model) into words. It's a full neural-network inference per segment of audio, which is why it's GPU-bound. Modern models also do language ID and are robust to accents/noise as part of the same pass.

Why this piece earns its place

The decoder's cost tracks the tokens it emits, not only the length of the audio, and that asymmetry is what needs guarding here. Hand a sequence model a segment with no speech in it and it will emit confident text, and can repeat itself until something stops it — a quality bug and a bill at once — so cap tokens per segment and treat a segment that hits the cap as suspect. Word-level timing is the other thing to settle before anyone promises it. A Whisper-style encoder-decoder emits coarse segment timestamps; per-word times come from a separate alignment pass, either dynamic time warping over the decoder's cross-attention or an external aligner. A CTC or transducer model aligns frame by frame and gives them away. So if captions or word-level search are in the requirements, that belongs in the argument at model-selection time, not downstream where it looks like a formatting detail. Placement matters at the front too: resampling, downmixing and the spectrogram are ordinary signal processing, small beside the forward pass, and running them on the accelerator mostly spends GPU the decoder could have used. Store the audio as it arrived, not the normalized copy, or every future re-run inherits today's preprocessing decisions.

  • mel spectrogramaudio features
  • seq2seqaudio → tokens
  • beam searchdecode to words

What the new pieces do

ASR Modeldata
The loaded acoustic/sequence model (Whisper-style). Maps audio features to a token sequence.

Step 5 · Feeding the model

Chunking & voice activity detection

You can't feed an open-ended live stream (or a one-hour file) to the model in one shot — models have a bounded input window, and streaming needs to emit results before the audio ends. How do you segment the audio well?

Gatewaystream · batchBatch Queueuploaded filesVAD / Chunkersegment audioASR Workeraudio → tokens
New in this step: VAD / Chunker.

You can't feed unbounded/live audio to a fixed-window model in one shot. How do you segment it?

  1. Hard fixed cuts slice words in half and lose the context that spans a boundary, hurting accuracy. You want boundaries at natural pauses and overlapping windows so no word is cut and context carries across.

  2. For batch that's fine, but for streaming you must emit partial results as speech happens — you can't wait for the utterance to end. Streaming requires incremental chunked decoding.

  3. VAD finds speech vs silence so you can segment at natural pauses (not mid-word), overlap windows so context spans boundaries, and — for streaming — decode each chunk as it arrives, emitting partial transcripts that may be refined as more audio comes in. This balances the model's bounded window against accuracy and latency.

Use voice-activity detection (VAD) to split audio into speech segments at natural pauses (not mid-word), with overlapping windows so context carries across boundaries. For streaming, decode each chunk incrementally as it arrives and emit a partial transcript that can be revised as more audio comes in; finalize when the segment ends. Chunk size is the core dial: smaller = lower latency but less context (lower accuracy), larger = the reverse.

Why this piece earns its place

Overlap is the right call, and it has a consequence: the same speech is decoded twice, and the two decodes will not agree. You need an explicit stitching rule — where the seam falls, which side wins — or the transcript repeats a word or two at every boundary. That defect hides completely if you measure accuracy per chunk instead of on the assembled transcript, which is the natural way to build the harness and the wrong one. VAD is a cost lever as well as an accuracy one, since audio with no speech in it is audio you never pay to decode, though what that saves depends on the traffic: one participant's own microphone is mostly silence, a mixed-channel meeting recording is not. The danger runs the other way too — a VAD tuned tight clips the quiet start of a word, so the failure arrives as missing first words rather than as an error anywhere. Chunk size belongs in the request, not the config, because live captions and dictation want opposite settings from the same service. And when the client already knows where an utterance ends, as with push-to-talk, it should say so, and you can skip detection for that stream.

  • VADsplit at pauses
  • overlapkeep context
  • partialsemit then refine

What the new pieces do

VAD / Chunkerservice
Voice-activity detection splits audio into speech chunks (overlapping windows) so the model gets bounded, meaningful segments.

Step 6 · Feed the fleet

Batched, autoscaling GPU workers

ASR inference is GPU-heavy, demand fluctuates, and you're paying for expensive accelerators. How do you run the worker fleet economically — while respecting streaming's latency?

Audio SourceGatewayBatch QueueVAD / ChunkerASR WorkerASR Model
The system as it stands at this step. · swipe to pan the diagram

The same GPU fleet serves both batch and streaming. Should both paths use the same batch size for inference?

  1. A single batch size forces a bad compromise: large enough for batch throughput means streaming waits for a batch to fill (killing latency), small enough for streaming means batch wastes GPU efficiency it doesn't need to waste.

  2. Batching raises throughput but adds latency by design (waiting to fill the batch). Batch jobs are latency-tolerant so they can use large batches for cost efficiency; streaming needs tiny, immediately-flushed batches to keep the sub-second budget, accepting lower GPU efficiency as the price.

  3. Never batching throws away most of a GPU's throughput advantage for the batch path, where nothing is waiting on a fast response — that's the one place batching is nearly free.

Batch multiple requests per GPU to raise utilization (with care: streaming needs small, fast batches to stay low-latency, while batch jobs can use large batches for throughput). Autoscale the fleet on load (queue depth for batch, active-stream count for real-time). Keep the multi-GB model resident (warm pool, avoid cold starts), and consider smaller/faster models or quantization for the streaming path where latency dominates and a slight accuracy trade is acceptable.

Why this piece earns its place

Streaming capacity is not counted in requests per second, and once VAD is gating the decoder it is not counted in GPU time either. What a live call occupies is a slot: a pinned worker, the decoder state for that stream, and an open connection, held for as long as the call lasts. So the fleet has two numbers to plan against — compute, which follows speech, and concurrency, which follows connected streams — and a fleet sized on utilization alone will look half idle right up to the moment it runs out of slots. Scaling down is the hard direction: you cannot drain a worker by waiting for its in-flight work to finish when one item is a call that may run for another hour, so admission stops on that worker first and the rest is patience. Scaling up is not free either, because a new worker loads multi-gigabyte weights before it serves anyone, which is what the warm pool is for and why streaming headroom is provisioned ahead of need. Batch, being queued and checkpointed, is the half that can take interruptible capacity. Two practical notes: unequal-length batches pad every item out to the longest, so bucket by duration or pay to decode padding; and a weights rollout cannot swap under an open stream without the transcript changing style mid-sentence, so keep both versions resident and let existing calls finish where they started.

  • batchthroughput (batch path)
  • small batchlatency (streaming)
  • autoscale + warmcost + no cold start

Step 7 · Make it useful

Post-processing

The model outputs raw tokens — often lower-cased, unpunctuated, with no idea who spoke or when. That's a wall of text, not a usable transcript. What turns tokens into a product?

Post-processpunct · diarizeTranscript Storeresults
New in this step: Post-process, Transcript Store.

Add a post-processing layer: punctuation & capitalization restoration, word-level timestamps (for captions/search), speaker diarization (who spoke which segment), language identification, and optional custom vocabulary / boosting for domain terms and names. This runs after (or alongside) decoding and is what makes a transcript readable, searchable, and attributable. It's a distinct value layer that can degrade gracefully if it fails.

Why this piece earns its place

Two different jobs share this one box. Punctuation, capitalization and the formatting of numbers and dates read the token stream and its timings, nothing else. Diarization and language ID read the waveform — voice activity, speaker embeddings, then clustering — so this stage wants the audio as well as the tokens, and that half is another neural pass over the sound, competing for the accelerators the previous step spends its time rationing. Post-processing is not the cheap tail of the pipeline it looks like; only its text half is. Formatting also breaks the timing map: three spoken tokens becoming one written 2023 collapses three timestamps into one, and captions and seek bars are driven by those timestamps, so decide what a formatted span's time means before someone builds on it. Recognition and diarization disagree about boundaries, too. Attach speakers per chunk and a turn that changes mid-segment is credited to whoever spoke most of it, producing a transcript that reads perfectly and is wrong about who said what; word-level attribution costs more and is what the feature is actually for. And the labels are anonymous — speaker one, speaker two. Turning them into names means enrolling voiceprints, which is biometric data in a way a transcript is not.

  • punctuation+ capitalization
  • timestamps + diarizationwhen + who
  • custom vocabdomain terms

What the new pieces do

Post-processservice
Adds punctuation, capitalization, word timestamps, speaker labels (diarization) and language ID to raw tokens.
Transcript Storedata
Durable store of finished transcripts (and optionally the audio), returned to the client or via callback.

Step 8 · The sharp edges

Latency, noise & cost

Real ASR fights hard problems: the streaming latency budget, noisy/overlapping/accented audio, long-form recordings that exceed the model window, and GPU cost.

Audio SourceGatewayBatch QueueVAD / ChunkerASR WorkerASR ModelPost-processTranscript Store
The system as it stands at this step. · swipe to pan the diagram

Hold the streaming latency budget with small chunks, fast/quantized models, and partial-then-refine output — accepting a small accuracy trade for responsiveness. Improve noisy audio with denoising/robust models and VAD, and handle overlapping speakers with diarization. Transcribe long-form audio by sliding the model window with overlap and stitching results. Control cost with batching, right-sized models per path, and caching/dedup of repeated audio. Fail gracefully — degrade post-processing before dropping the core transcript.

Why this piece earns its place

Word error rate is one number over a whole corpus, and the tokens that decide whether a transcript is usable — names, numbers, product terms — are a small minority of them, so a model can improve on average and get worse at exactly what a customer checks first. Keep separate evaluation sets per condition: phone audio, accents, background noise, each scored on its own, because a robustness gain on clean read speech will otherwise hide a regression on the traffic you actually serve. There is a versioning consequence too. Transcripts get compared: a customer who re-runs last month's recording and gets different words has found a bug as far as they are concerned, so record the model version beside every transcript and be able to serve a pinned one. And set the accuracy bar per product rather than buying the best model everywhere — a search index over recordings tolerates errors a legal or medical transcript never will, and that choice, not the GPU, is the real cost control.

You did it

You just designed a speech-to-text service.

Audio SourceGatewayBatch QueueVAD / ChunkerASR WorkerASR ModelPost-processTranscript Store
The finished design, end to end. · swipe to pan the diagram

Everything you assembled, in order

  • Batch (accuracy, latency-tolerant) and streaming (real-time, low-latency) are opposite constraints — split them.
  • A gateway routes streaming to a persistent connection and batch to an async job queue, sharing one GPU fleet.
  • Keep batch and streaming on isolated paths so a batch flood can't starve real-time latency.
  • The model turns a mel spectrogram into text tokens — GPU-bound seq2seq inference.
  • VAD + overlapping chunks + incremental partials feed the fixed model window and trade latency vs accuracy.
  • Batch on GPUs for throughput but keep streaming batches small; autoscale and keep weights warm.
  • Post-processing (punctuation, timestamps, diarization, custom vocab) turns raw tokens into a usable transcript.

Deep cut · 14:43

Live can never wait.

The interactive build above lays out a speech-to-text service: two entrances, a cutter, a GPU worker pool and a finishing step. The film shows why it is shaped that way. A live game show freezes its captions mid-word, and an hour earlier the same board wrote the wrong words and then corrected itself. It looks like two bugs and it is one design question: live audio cannot wait for the end of a sentence, and it cannot wait for a free GPU. So the captions must be allowed to change their mind, and a few GPUs must be reserved so a flood of recordings can never take their seats.

  • See the two entrances: a ticket queue for recordings and an open line for live captions, with a cutter that slices long audio in the quiet gaps and overlaps the pieces so no word is chopped.
  • Take it into the interview: name voice activity detection, overlapping chunks, the mel spectrogram, GPU workers, partial versus final results, speaker diarization and reserved capacity. Ask first: is this live, or is it a recording?

Where an interviewer pokes next

Getting the boxes right is the easy half. These are the questions that separate a candidate who drew the diagram from one who has run the thing. Answer each one out loud before you open it.

  1. Why split batch and streaming into separate paths instead of one transcription API?

    Because they optimize for opposite things and a single naive design can't satisfy both. Batch transcription (an uploaded podcast, a recorded meeting) is latency-tolerant but accuracy-first: it can wait, buffer the whole recording, use large context windows and big decoding batches, and take minutes of GPU time to produce the best possible transcript. Streaming transcription (live captions, a voice assistant) is the opposite: it must emit words within a fraction of a second of them being spoken, before the sentence is even finished, which means holding a persistent connection, decoding audio incrementally in small chunks, and emitting partial transcripts that may be revised as more audio arrives. These constraints demand different plumbing — an async job queue for batch versus a low-latency streaming connection with partial decoding — even though both share the same underlying model and GPU fleet. Splitting them at the gateway lets each meet its own SLA, and isolating their capacity ensures a flood of batch uploads can't consume the GPUs that real-time callers depend on.

  2. What does the ASR model actually do to turn audio into text?

    It runs a sequence-to-sequence inference: audio in, text out. First the raw waveform is converted into audio features, almost always a mel spectrogram, which is a time-frequency representation that captures how energy across frequency bands changes over time — a much more model-friendly input than raw samples. That spectrogram is fed to the ASR model, typically a Whisper-style encoder-decoder or a transducer, whose encoder builds a representation of the audio and whose decoder emits a sequence of text tokens, usually with beam search (and sometimes an auxiliary language model) to choose the most likely word sequence. Modern models fold in extras like language identification and robustness to accents and background noise within the same pass. The whole thing is a full neural network evaluation over a segment of audio, and its compute scales with the audio length and model size, which is why it's GPU-bound and why how you segment the audio directly drives both latency and cost.

  3. Why is chunking with VAD and overlap so central to streaming ASR?

    Because you can neither feed unbounded live audio to a fixed-window model in one shot nor wait for speech to finish before responding, so you must segment the audio intelligently. Naive fixed-length slicing is bad: hard boundaries cut words in half and discard the context that spans a boundary, both of which hurt accuracy. Voice-activity detection solves the boundary problem by distinguishing speech from silence, letting you cut at natural pauses rather than mid-word, and overlapping the windows preserves the context that would otherwise be lost across a cut so the model can still condition on surrounding audio. For streaming specifically, you then decode each chunk incrementally as it arrives and emit a partial transcript immediately, revising it as more audio comes in and finalizing when the segment ends — which produces the familiar experience of words appearing as you speak and then settling. Chunk size becomes the fundamental tuning knob: smaller chunks lower latency but give the model less context and thus lower accuracy, while larger chunks do the reverse, so you tune it to each product's latency-versus-accuracy needs.

  4. How do batching needs differ between the batch and streaming paths on the same GPUs?

    They pull in opposite directions on the same lever. GPUs are throughput machines that are most efficient when processing many inputs in parallel, so batching multiple requests together dramatically raises utilization and lowers cost per transcript — which is exactly what you want for the batch path, where jobs are latency-tolerant and can be grouped into large batches. But batching inherently adds latency, because you either wait to accumulate a full batch or process more work per step, and that directly conflicts with streaming's requirement to emit words within a fraction of a second. So the streaming path uses small, promptly-flushed batches (or even near-single-request processing) to keep latency low, accepting lower GPU efficiency as the price of responsiveness, while the batch path uses large batches for maximum throughput. On top of this, you autoscale the fleet based on load (queue depth for batch, active-stream count for streaming), keep the multi-gigabyte model weights resident in a warm pool to avoid slow cold starts, and often run a smaller or quantized model on the streaming path where latency dominates and a slight accuracy trade is acceptable. The unifying idea is that batching optimizes throughput/cost but hurts latency, so you apply it aggressively where latency doesn't matter and sparingly where it does.

  5. Why is post-processing a distinct layer, and what does it add?

    Because the raw output of an ASR model is a sequence of recognized word tokens — often lower-cased, with no punctuation, no sentence boundaries, no indication of who spoke, and no timing — which is technically correct recognition but a poor product: a hard-to-read wall of text you can't search, caption, or attribute. Post-processing is the layer that turns those tokens into a usable transcript. It restores punctuation and capitalization so the text reads naturally; it adds word-level timestamps so the transcript can drive captions, be aligned to the audio, or be searched by time; it performs speaker diarization to label which speaker said which segment (a separate model answering "who spoke when"); it does language identification; and it applies custom vocabulary or biasing so domain-specific names, jargon, and acronyms that a generic model would mishear are recognized correctly. These are distinct steps and models composed on top of recognition, which is why they can degrade gracefully — if post-processing fails you can still return the core transcript, just less polished — and why the product value of a speech-to-text service lives substantially in this layer, not only in raw recognition accuracy.

Check yourself — the answers, and why

Nine steps in, these are the calls you should be able to make cold. Pick one, then read why.

  1. Speech-to-text splits into two modes because…

    Batch is an async job; streaming needs low-latency incremental partial transcripts.

  2. The ASR model turns audio into text by…

    Mel features in, text tokens out — GPU-bound sequence-to-sequence inference.

  3. VAD + overlapping chunks are used to…

    Chunk size trades latency vs accuracy; incremental partials give the live-caption feel.

  4. Batch and streaming are kept on separate paths so that…

    Incompatible SLAs on shared GPUs need isolation (separate queues / reserved capacity).

  5. Post-processing adds…

    Raw tokens become readable, searchable, attributable text; degrades gracefully if it fails.

How you’d open this design in an interview

Before any boxes: agree what it must do, pin the qualities that shape everything, then build — naming each trade-off as you make it. The walkthrough above is that exact order.

What it must do

Agree on these before drawing a single box.

  • Batch transcribe: turn an uploaded recording into an accurate transcript as an async job.
  • Stream transcribe: caption live audio, emitting words within a fraction of a second as they’re spoken.
  • Run the model: convert a waveform to a mel spectrogram and decode it to text tokens on a GPU.
  • Segment: use VAD to split audio at pauses with overlapping windows, decoding streams incrementally.
  • Post-process: add punctuation, word timestamps, speaker diarization and custom vocabulary.

The qualities that shape everything

Each one names the mechanism that buys it.

Serve accuracy-first and real-time from one model
Recognize batch and streaming as opposite constraints and split them — an async job for batch, an incremental low-latency connection for streaming — sharing the model.
A batch flood can’t starve real-time
Route batch uploads through a job queue on a path separate from streaming (reserved capacity/priority), so bursts absorb as backlog without touching the interactive lane.
Feed a fixed model window well
VAD splits audio at natural pauses with overlapping windows and streaming decodes chunks incrementally into revisable partials — chunk size trades latency vs accuracy.
Run expensive GPUs economically
Batch many requests per GPU on the batch path but keep streaming batches small and prompt-flushed; autoscale on load and keep weights warm to avoid cold starts.
Raw tokens become a usable product
A post-processing layer restores punctuation and capitalization, adds timestamps, speaker diarization, language ID and custom vocab — a distinct value layer that degrades gracefully.
Hold the budget under noise + long audio
Small chunks + fast/quantized models + partial-then-refine for latency, denoise/robust models for noise, and a sliding window with stitching for long-form recordings.

The trade-offs you say out loud

Senior signal isn’t the boxes — it’s naming what you gave up and why it was the right price.

Split batch and streaming at the front door over one naive upload → transcribe → return design

Batch is accuracy-first and latency-tolerant; streaming must emit words within a fraction of a second before the sentence ends. Opposite priorities need different plumbing — async job vs low-latency incremental connection — even sharing the model.

Separate queues / reserved capacity over one undifferentiated GPU pool

Batch and streaming compete for the same scarce GPUs but have incompatible SLAs; a shared pool lets a batch flood ruin real-time latency. Isolation protects the interactive path.

VAD + overlapping windows + partials over fixed 1-second slices with hard boundaries

Hard fixed cuts slice words in half and lose context that spans a boundary, hurting accuracy. VAD cuts at natural pauses, overlap carries context across, and revisable partials give the live-caption feel.

Large batches for batch, tiny for streaming over a single batch size across both paths

One batch size forces a bad compromise — large enough for throughput makes streaming wait for a batch to fill (killing latency); small enough for streaming wastes GPU efficiency batch doesn’t need to lose. Size batching to each path’s SLA.

Small chunks + fast/quantized model on streaming over the full-size accuracy-first model everywhere

The streaming path is latency-dominated, so a slightly smaller/faster model and small chunks hold the sub-second budget; the batch path keeps the big model where accuracy matters and nothing waits on a fast response.

The answer, out loud

What a strong answer to “Design a Speech-to-Text Service” sounds like, first question to last trade-off. It is about 7 minutes of talking; the whiteboard and the interviewer fill the rest of the 45. Read it aloud once, then close the page and give it yourself.

  1. 0–3 min

    Name the two products first

    Before I draw anything I want to split the requirement, because “transcribe audio” is really two products. One is batch: somebody uploads a recording — a podcast, a meeting — and wants the most accurate transcript we can produce, and they’ll wait for it. The other is streaming: live audio, captions that have to appear within a fraction of a second of the words being spoken, before the sentence is even finished. Same model underneath, opposite priorities. So the first thing I’d ask is which of those we’re on the hook for, and what “live” means to the people asking for it — captions running inside a call, or a transcript that turns up once the call is over.

  2. 3–8 min

    Why one naive design can’t serve both

    If I built the obvious thing — upload, transcribe, return — batch works and streaming doesn’t. Streaming can’t wait for the recording to end, because there isn’t one; it has to emit words as they’re spoken and refine them afterwards. So the two need different plumbing: an async job for batch, a persistent low-latency connection with incremental decoding for streaming. They share the model and the GPU fleet, but I’d split them right at the front door, and that split is the decision everything else in the design hangs off.

    Built in step 1: Batch vs streaming
  3. 8–14 min

    A gateway with two front doors

    So the front door is one gateway with two modes, and what separates them is flow control. A batch upload is audio at rest — it’s all there already — so I take the file, queue a job, and hand back an id to poll or a callback URL; the caller waits, and that’s fine. A live stream arrives at the speed someone is talking, and I can’t ask them to slow down while a GPU catches up, so that door holds the connection open and sends text back as the frames move through. One expensive fleet behind both. The asymmetry I’d call out is that the batch door can say “later” and the streaming door never can — which is why I wouldn’t serve them down one path.

    Built in step 2: A gateway that splits the paths
  4. 14–20 min

    Uploads become queued jobs

    Behind the batch door I’d put a job queue. An hour of audio is minutes of GPU work and uploads arrive in bursts, so the queue absorbs the burst as backlog: ETAs stretch, nothing is dropped, and workers drain it at the fleet’s sustainable rate. The part I’d insist on is that batch and streaming stay on separate paths — separate queues, or reserved capacity. They compete for the same scarce GPUs with incompatible SLAs, and in one undifferentiated pool a flood of uploaded files will quietly wreck real-time latency for everyone on a live call.

    Built in step 3: Async jobs for uploads
  5. 20–26 min

    What the worker actually runs

    Let me say what’s actually inside a worker, because the shape of everything else follows from it. I’d take the raw waveform, turn it into audio features — a mel spectrogram — and hand that to the model, something Whisper-style, which encodes it and decodes out a sequence of text tokens, usually with beam search. That’s a full neural network pass over a segment of audio, so it’s GPU-bound, and the compute follows how much audio I hand it. The consequence I’d flag is that segmentation becomes a first-class decision rather than a detail: the size of the piece I feed the model sets how quickly a word comes back and what the transcript costs.

    Built in step 4: The ASR inference pipeline
  6. 26–33 min

    Cutting the audio so the model can eat it

    I can’t hand an open-ended live stream to a fixed-window model, so I’d use voice-activity detection to cut at natural pauses rather than mid-word, with overlapping windows so context carries across a boundary. For streaming, each chunk is decoded as it arrives and emitted as a partial that can be revised, then finalized when the segment ends — that’s the “words appear, then settle” feel people expect from captions. On the batch side I’d segment the same way but I don’t have to be clever about it: nothing is waiting, so I can afford wider windows and more overlap and buy accuracy with time I’m allowed to spend.

    Built in step 5: Chunking & voice activity detection
  7. 33–39 min

    Making the GPUs affordable

    The GPUs are the bill, so I’d size the two halves against different numbers. Batch work is queued and restartable, so that’s the elastic half: big batches for throughput, scaled on the backlog measured in seconds of audio rather than jobs, because a job count says nothing about the work waiting. Streaming I’d plan as concurrent calls, since a call holds its worker for as long as it runs, and I’d keep spare capacity standing rather than react, because a fresh worker loads multi-gigabyte weights before it is any use. Batching is where they fight: on the batch path it’s nearly free throughput, on the streaming path it’s latency I don’t have, so streaming gets small batches flushed immediately and I take the worse utilization on purpose. If I were still over budget, I’d try a smaller or quantized model on the streaming path first; that side is already trading accuracy for speed.

    Built in step 6: Batched, autoscaling GPU workers
  8. 39–45 min

    Post-processing, the trade-off, and what I’d do next

    Last, raw tokens aren’t a product — they come out lower-cased and unpunctuated, with no idea who spoke or when. So a post-processing layer restores punctuation and capitalization, adds word-level timestamps, does speaker diarization and language ID, and applies custom vocabulary for the domain names a generic model mishears. Because it’s a distinct layer it can degrade on its own: if it fails I’d still return the transcript, just less polished. The trade-off running through the whole design is latency against accuracy — chunk size, batch size, model size are the same argument in different clothes — and the isolation between the two paths is what stops one side’s choice becoming the other side’s problem. With more time I’d go after overlapping speakers and diarization quality, and long-form recordings that exceed the model window, where you slide and stitch.

    Built in step 7: Post-processing

What this teaches

Learn AI system design by building a speech-to-text / ASR service like Whisper-at-scale, Deepgram or Google Speech step by step. An interactive guide covering batch vs streaming transcription, the ASR inference pipeline, chunking and voice activity detection, a batched autoscaling GPU fleet, post-processing (punctuation, diarization, timestamps), and the unhappy paths (real-time latency budget, accuracy vs chunk size, noisy audio, cost).

Key takeaways

  • Batch (accuracy, latency-tolerant) and streaming (real-time, low-latency) are opposite constraints — split them.
  • A gateway routes streaming to a persistent connection and batch to an async job queue, sharing one GPU fleet.
  • Keep batch and streaming on isolated paths so a batch flood can't starve real-time latency.
  • The model turns a mel spectrogram into text tokens — GPU-bound seq2seq inference.
  • VAD + overlapping chunks + incremental partials feed the fixed model window and trade latency vs accuracy.
  • Batch on GPUs for throughput but keep streaming batches small; autoscale and keep weights warm.
  • Post-processing (punctuation, timestamps, diarization, custom vocab) turns raw tokens into a usable transcript.

Concepts covered

  • What makes speech-to-text hard?
  • Batch vs streaming
  • A gateway that splits the paths
  • Async jobs for uploads
  • The ASR inference pipeline
  • Chunking & voice activity detection
  • Batched, autoscaling GPU workers
  • Post-processing
  • Latency, noise & cost
RUN IT YOURSELF

The word the acoustic model got right and the decoder threw away

One frame-level acoustic posterior, decoded two ways — greedy argmax with CTC blank collapsing, and prefix beam search — and both scored against the reference by a real word-level edit distance, running for real in your browser. It prints WER side by side, names the words each decoder loses, scores the two transcripts on the same posterior to show why, and then re-lays that posterior at other frame rates to show where the whole effect stops. Set BEAM to 1, hit Run, and the beam row collapses onto greedy.

HOW TO READ THE CODE — 5 IDEAS
  1. The model is a seq2seq pass over a spectrogram; decoding is the separate search that reads its output (step 4). Same posterior here, two searches, 0.600 vs 0.200 WER.
  2. Greedy takes the argmax path. In every frame of a rushed phoneme blank outscores the letter, so the best path skips a letter the best transcript keeps — and the printed columns show greedy winning best path while losing all paths.
  3. Accuracy is bought with compute — 1,652 symbol scans greedy, 19,516 at width 12. Width 1 spends exactly greedy’s 1,652, which is how you know both columns are one unit. That is the batch-vs-streaming dial again: a latency-tolerant job can afford the search, a sub-second caption cannot (steps 6, 8).
  4. Beam does not fix the model: send → sand costs both decoders identically, because no search outvotes a posterior that is confidently wrong. The two words only greedy loses are an amount and a name — the tokens a corpus-wide WER average hides and a customer checks first (step 8).
  5. The effect has an edge, and the last rows print it. Change FRAMES_PER_CHAR: at 4 the split still holds, at 6 beam returns "fiftteen" and ties greedy. Beam is not wrong there — a longer run gives a blank more interior places to fall, and the doubled label really does outscore the single one. CTC has no length prior; supplying one is the lexicon or language model of step 5.
CPython · WebAssembly
built to turn a stream of sound into words as they are spoken — make the calls, flood the queue, run the gauntlet.
Finished this one? 0 / 65 AI System Designs done

Explore the topic

See this alongside everything else on the same subject — handbooks, system designs, challenges and tools, in one place.

More AI System Designs