●AUTO — Auto mode becomes the default in Claude Code tomorrow, August 14, across the Pro, Max, and Team plans●SUNSET — The legacy Workbench and the experimental prompt tool APIs retire on August 17, now four days away●COWORK — Cowork has expanded to mobile and web, so sessions and files follow you between devices, with background runs, scheduled tasks, and approvals from your phone●DESIGN — Claude Design is here: build branded decks, landing pages, and prototypes in one conversation, then export to PDF, PPTX, Canva, or HTML●AUDIT — The Compliance API now covers Cowork and Claude Code across desktop, web, mobile, and CLI, in beta for Enterprise customers●PRICE — Sonnet 5 promo pricing at $2/$10 per Mtok runs through August 31, moving to $3/$15 on September 1●AUTO — Auto mode becomes the default in Claude Code tomorrow, August 14, across the Pro, Max, and Team plans●SUNSET — The legacy Workbench and the experimental prompt tool APIs retire on August 17, now four days away●COWORK — Cowork has expanded to mobile and web, so sessions and files follow you between devices, with background runs, scheduled tasks, and approvals from your phone●DESIGN — Claude Design is here: build branded decks, landing pages, and prototypes in one conversation, then export to PDF, PPTX, Canva, or HTML●AUDIT — The Compliance API now covers Cowork and Claude Code across desktop, web, mobile, and CLI, in beta for Enterprise customers●PRICE — Sonnet 5 promo pricing at $2/$10 per Mtok runs through August 31, moving to $3/$15 on September 1
Production Voice Agents with Claude API: Latency Budgets, Cost, and Fallbacks
Orchestrating Whisper/Deepgram, Claude API, and TTS into a voice agent that survives production — latency budgets measured on Cloudflare Workers and Cloud Run, per-session cost math, three-tier fallbacks, and barge-in handling.
The first time I put a voice agent in front of real users, the replies that had felt snappy in testing sounded noticeably slow on a real phone. The measurement said p95 was 1.8 seconds — only 300ms over budget.
Listeners still felt the pause. They repeated themselves, the repeat produced a duplicate transcript, and the duplicate made the next turn slower. A small overshoot on paper, a broken conversation in practice.
The hard part of voice agents is not model quality. It is how you spend time. Claude API handles the language reasoning; speech recognition and speech synthesis live in separate services, and milliseconds pile up at every seam between them.
What follows is a full stack — Whisper or Deepgram for input, Claude API for reasoning, several TTS engines for output — organized around four decisions: latency budget, cost math, failover, and monitoring. Everything is TypeScript/Node.js, with code you can run as written.
Voice Agent Architecture Overview
A production voice agent system spans multiple integrated layers:
Let's build each piece methodically, starting with speech recognition.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦How I split a 1500ms voice-to-voice latency budget into STT 600ms / Claude Haiku 400ms / TTS 400ms, and compressed real-world p50 to 910ms with Deepgram Streaming
✦Reduced per-session cost from $0.024 to $0.011 across 4 specific decisions, with the Sonnet routing rule that finally worked after Sonnet-judges-itself failed
✦Why Cloudflare Workers cannot host the inference layer, and the Durable Objects + Cloud Run split I run in production with signed JWT session tokens
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
Voice agents need brief, action-oriented system prompts. Users can't easily re-read long responses:
const VOICE_AGENT_SYSTEM_PROMPT = `You are a helpful, conversational voice assistant.Guidelines:- Respond naturally, as if speaking to someone. Keep sentences short (under 20 words when possible).- Use conversational language. Avoid jargon unless the user introduced it first.- If unsure, admit it. Don't speculate.- Break complex information into bullet points (max 3 items per response).- No emojis. No markdown formatting. Speak like a real person.- Be warm and encouraging while remaining professional.`;
Text-to-Speech Implementation and Optimization
Multi-Provider TTS Adapter Pattern
Production systems need failover. Implement a provider-agnostic interface:
// src/monitoring/metrics.tsimport prom from 'prom-client';export const voiceAgentMetrics = { totalSessions: new prom.Counter({ name: 'voice_agent_total_sessions', help: 'Total number of sessions', labelNames: ['status'] // success, failed, timeout }), apiCallsTotal: new prom.Counter({ name: 'voice_agent_api_calls_total', help: 'Total API calls by service', labelNames: ['service'] // claude, whisper, tts }), sessionDurationSeconds: new prom.Histogram({ name: 'voice_agent_session_duration_seconds', help: 'Session duration in seconds', buckets: [10, 30, 60, 300, 600] }), apiLatencyMs: new prom.Histogram({ name: 'voice_agent_api_latency_ms', help: 'API latency in milliseconds', labelNames: ['service'], buckets: [50, 100, 200, 500, 1000, 2000] }), activeSessions: new prom.Gauge({ name: 'voice_agent_active_sessions', help: 'Number of currently active sessions' })};export function recordSessionMetric(durationSeconds: number, success: boolean): void { voiceAgentMetrics.totalSessions.inc({ status: success ? 'success' : 'failed' }); voiceAgentMetrics.sessionDurationSeconds.observe(durationSeconds);}
Seven lessons that aren't in the official docs
The sections above are the design story. Below are the things I only learned by running this stack in production as a solo developer — the failure modes that never appear in the official docs.
1. Measure the latency budget in three layers (1500ms total)
End-to-end voice-to-voice latency above 1500ms breaks the conversational rhythm. Users start asking "did it cut out?" and re-speak before the agent can answer. That number is the threshold I keep walking back to across every voice product I have shipped.
My production budget split, measured on Cloudflare from Tokyo to us-east-1:
Layer
Budget
p50
p95
STT (Deepgram Streaming)
600ms
280ms
480ms
Claude Haiku response
400ms
320ms
620ms
TTS (ElevenLabs Flash v2)
400ms
240ms
410ms
Network round-trip
100ms
70ms
130ms
Total
1500ms
910ms
1640ms
If you call Whisper REST naively, inference only fires after the full audio clip lands, which adds 600 to 900ms after the last syllable. Deepgram Streaming uses VAD to predict the endpoint, which cuts perceived latency roughly in half. I started with Whisper REST for simplicity, watched p95 exceed 1800ms, and migrated to Deepgram Streaming three weeks later.
// Production budget checker — emit Sentry warnings on any over-budget sessioninterface LatencyBudget { stt: { budget: 600; actual?: number }; llm: { budget: 400; actual?: number }; tts: { budget: 400; actual?: number }; network: { budget: 100; actual?: number };}export function assertBudget(b: LatencyBudget, sessionId: string) { const total = (b.stt.actual ?? 0) + (b.llm.actual ?? 0) + (b.tts.actual ?? 0) + (b.network.actual ?? 0); if (total > 1500) { console.warn(`[budget-exceeded] session=${sessionId} total=${total}ms`, b); } return total <= 1500;}
2. How I cut per-session cost from $0.024 to $0.011
I price every feature in dollars-per-session before writing the first line of code — a habit left over from years of running ad-supported apps. The first build (Whisper + Sonnet + ElevenLabs Multilingual) ran roughly $0.024 per 3-minute session. Over 9 weeks I brought it to $0.011 with four decisions.
Decision
Before
After
Reduction
STT: Whisper → Deepgram Nova-2
$0.006/min
$0.0043/min
-28%
LLM first-pass: Sonnet → Haiku
$3/1M tok
$0.25/1M tok
-91%
Sonnet only for "complex" queries
100% Sonnet
18% Sonnet
-82%
TTS: ElevenLabs Multilingual → Flash v2
$0.30/1k chars
$0.10/1k chars
-67%
The Sonnet routing rule deserves its own warning. My first attempt was to let Claude itself judge complexity, but the judge ran on Sonnet too, so the savings vanished. The rule that actually works in production is dumb on purpose: input over 80 characters, OR a technical term in the last 3 turns, OR the system prompt explicitly escalated. Simple rules beat clever judges.
3. Cloudflare Workers cannot be the inference layer
My Next.js sites already run on Cloudflare Workers + OpenNext, so my first instinct was "let me put the voice agent there too." It does not work, for three concrete reasons.
30-second CPU limit: every conversation turn burns 1–2s of CPU, and long sessions exceed the limit. Durable Objects share the same quota.
WebSocket constraints: Workers WebSockets cap individual frames at 32KB and disconnect at 16 hours. Bidirectional audio streaming requires Durable Objects + Hibernation API, which is much more design overhead than people assume.
Audio library bundling: ffmpeg WASM builds usually push you past the 10MB Worker bundle limit.
What I run today: Cloudflare Workers for signaling and session management on Durable Objects, and Google Cloud Run for the audio processing + Anthropic API path. The Cloudflare side still owns membership gating (premium_token cookie), and only paid users get a signed JWT for the Cloud Run WebSocket.
// Workers issues a short-lived JWT; Cloud Run verifies it on WebSocket upgradeimport { SignJWT } from 'jose';export async function issueVoiceSessionToken(env: Env, userId: string) { const secret = new TextEncoder().encode(env.VOICE_SESSION_SECRET); return await new SignJWT({ sub: userId, scope: 'voice-session' }) .setProtectedHeader({ alg: 'HS256' }) .setIssuedAt() .setExpirationTime('15m') .sign(secret);}
4. A three-tier fallback so I never get paged at 2am
Both of my grandfathers were temple carpenters in Japan. Their rule was "fix what you can fix before you go home, even in the rain." I apply the same rule to production: assume every dependency will fail, and have three layers ready.
Claude failure: Sonnet → Haiku → pre-recorded "Sorry, could you say that again" TTS (cost near zero)
TTS failure: ElevenLabs → OpenAI TTS → Browser Web Speech API (audio quality hit, still usable)
Whether to tell the user about the degradation is a product decision. For free assistants I stay silent; for membership users I display "running in simplified mode" so the trust signal stays intact. Honesty is the foundation of paid membership.
5. Hold conversation history as a graph, not a flat array
Voice users cannot consciously segment context the way chat users do. "Wait, not that one, the earlier one" happens constantly. My first version stored a flat array and dumped it into the Claude context window. Even when I stayed under 200k tokens, answer quality drifted because recent turns started leaking influence into older turns.
I now hold the conversation as three node types:
Question node: the user's intent plus the answer the agent gave
Topic node: an intermediate node that groups question nodes about the same subject
State node: an explicit state transition like "booking → awaiting confirmation → canceled"
Each Claude call receives the last 6 turns in full + the current topic-node summary + the state node only. Effective context fits in 4k–8k tokens and my eval set shows 30–40% accuracy improvement on multi-topic conversations.
6. Split UX and cost dashboards in Grafana
Prometheus + Grafana is the obvious choice, but the production lesson is: never put UX metrics and cost metrics on the same board. When p95 latency spikes, you need to ask "should I roll back the Haiku-first routing for cost?" without the cost number pulling your eye.
Cost board: avg cost per session, STT/LLM/TTS share, Sonnet ratio, free vs. premium unit-cost gap
A Grafana variable for tier=free|premium makes it fast to ask "does cost-cutting hurt the paying users?" — the question I check first every morning on anything with a paid tier behind it.
7. Kill barge-in at the playback buffer, not at the TTS call
The loudest complaints in production were not about latency or recognition accuracy. They were about what happens when a user starts talking while the agent is still speaking.
My first fix was the obvious one: abort the server-side TTS request as soon as VAD detects speech. It barely helped. The client already holds 400 to 800ms of audio in its playback buffer, so stopping the source does nothing for audio that has already been delivered — the agent talks over the user anyway.
The right place to cut is the playback side.
// Client: when VAD fires, kill local playback firstexport function createBargeInController(ws: WebSocket) { let current: AudioBufferSourceNode | null = null; return { play(node: AudioBufferSourceNode) { current = node; node.start(); }, onUserSpeechStart() { current?.stop(); // 1. silence locally, within ~20ms current = null; ws.send(JSON.stringify({ type: 'barge_in' })); // 2. then tell the server }, };}
Order matters. Notify the server first and the 70–130ms round trip stays audible, which users hear as "my interruption did nothing."
On the server, receiving barge_in means aborting the TTS stream and rewriting conversation history to contain only the text the user actually heard. Store the full response and Claude will build the next turn on content nobody listened to, which is how voice conversations go subtly off the rails.
I attach a character offset to every TTS streaming chunk, persist the assistant turn only up to the interruption offset, and append (interrupted here — user began speaking). That single line is enough for Claude to pick the thought back up instead of restarting it.
What I rely on when I translate the design into code
Numbers and code matter, but the question I lead with is "what am I trading my own time for?" My budget — p95 1500ms, $0.015 per session — exists so I can decide in 30 seconds whether a 2am alert needs me out of bed. Without a line drawn in advance, every alert feels equally urgent, and the deciding itself is what wears you down.
Voice agents demand more "humanness" than text chat. Being fast, cheap, and reliable at the same time is fundamentally hard, but layering budgets, holding three tiers of fallback, and splitting the dashboards keeps the on-call rotation survivable.
Thank you for reading this far. If you are building voice agents on a similar stack, I hope these numbers and decisions save you a few weekends.
Share
Thank You for Reading
Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.