SEC 08 / PRODUCTION
Production architecture & failure modes
Everything so far has been about picking the right transport and the right package. This page is about what a real deployment looks like once you put those pieces together — the recommended shape of the system, an adapter pattern that keeps you from getting locked into any one SDK version, and the specific ways integrations tend to break in practice.
The recommended architecture
┌───────────────────────────┐
│ Browser or mobile client │
│ │
│ Agent SDK / Scribe SDK │
└─────────────┬─────────────┘
│
ephemeral credential
│
┌─────────────────────────▼─────────────────────────┐
│ Application backend │
│ │
│ Authentication and authorization │
│ ElevenLabs SDK adapter │
│ Key and secret management │
│ Token/signed-URL issuance │
│ Request logging and rate limits │
│ Job state, caching, and webhook handling │
└───────────┬───────────────────────┬───────────────┘
│ │
REST / stream Webhook receiver
│ │
┌─────▼───────────────────────▼─────┐
│ ElevenLabs │
│ REST · WebSocket · WebRTC │
│ TTS · STT · Agents · Media │
└───────────────────────────────────┘
The permanent API key remains in the application backend. The client receives only a short-lived credential for a specific authorized real-time session. Ordinary TTS, STT uploads, voice management, and agent administration remain server-side.
The internal adapter
Your application should depend on an interface such as:
interface VoicePlatform {
createSpeech(input: CreateSpeechInput): Promise<SpeechResult>;
streamSpeech(input: StreamSpeechInput): Promise<AsyncIterable<Uint8Array>>;
transcribe(input: TranscriptionInput): Promise<Transcript>;
createRealtimeCredential(
input: RealtimeCredentialInput,
): Promise<RealtimeCredential>;
}
Code elsewhere in your application talks to VoicePlatform, never to the
generated ElevenLabs client directly. The adapter can then centralize:
- SDK construction;
- package-version compatibility;
- model selection;
- output formats;
- timeout and retry policy;
- error translation;
- request-ID capture;
- logging redaction;
- metering;
- fallback behaviour.
That last point matters more than it looks: it means an SDK major-version bump, or a swap from one transport to another, is a change to one module instead of a change scattered across every call site.
Common integration failures
These are the failure patterns worth designing against up front, rather than discovering in production.
Using the server package in the browser
The broad Node REST SDK is not a substitute for the browser Agents SDK. Beyond bundle size and environment mismatches, exposing the permanent API key to a browser client is a serious credential flaw — it’s exactly the short-lived-credential boundary in the architecture diagram above, broken.
Confusing HTTP streaming with WebSocket TTS
HTTP streaming assumes the complete request is sent at the start. The WebSocket allows later text chunks to arrive over time. Reaching for the WebSocket merely because the requirement mentions “streaming” can add session complexity — connection lifecycle, buffering, generation triggers — that ordinary HTTP streaming doesn’t need. See the six transport patterns for the full decision tree.
Letting generated SDK types permeate the application
Generated method names and models can change between releases. Wrapping the
client in an internal service boundary — the VoicePlatform adapter above —
means an upgrade touches one module instead of every call site across the
codebase.
Depending on SDK playback helpers
Helpers that shell out to MPV, FFmpeg, PyAudio, or an operating-system audio device are convenient for demos. They are poor abstractions for containers, serverless runtimes, and production web servers, which typically have no audio device to play through at all.
Failing to normalize numbers
This is especially visible with Flash v2.5. Expand dates, currencies, units, acronyms, telephone numbers, and unusual identifiers before they reach the model when pronunciation matters, rather than trusting the model to guess correctly.
Not testing audio teardown
Real-time media applications commonly fail not during the first connection but during:
- a second connection;
- rapid start/stop;
- device switching;
- permission revocation;
- component unmount;
- browser tab suspension;
- backgrounding;
- connection loss.
The official Agents repository has tracked issues involving AudioContext
state, microphone release, and React Native/browser media behaviour —
reinforcing that lifecycle testing needs to be explicit, not incidental.
Treating agent tools as trusted commands
Tool calls must pass through the same authorization and validation layer as ordinary user actions. A conversation model should never directly control billing, account deletion, money transfer, email sending, or sensitive record access without an application-level safeguard sitting between the tool call and the effect.
Depending on unpinned examples
The Agents APIs are evolving, and an official next-major migration is already documented. Pin the package, read examples from the matching tag, and avoid “latest branch” documentation when maintaining stable production code.
Going deeper: errors, retries, and observability
The API uses standard HTTP status codes and includes structured details — error type, error code, human-readable message, affected parameter, and a request ID. Log that request ID alongside your own trace ID, but never log API keys or raw audio.
| Condition | Typical treatment |
|---|---|
| 400 / 422 | Correct the request; normally do not retry |
| 401 / 403 | Configuration, credentials, or permissions problem |
| 402 | Credit or billing condition |
| 404 | Invalid or deleted voice, model, agent, or resource |
| 408 | Retry with bounded exponential backoff |
| 409 | Retry only when the operation is safe to repeat |
| 429 rate limit | Delay according to rate policy |
| 429 concurrency limit | Reduce simultaneous generation workload |
| 5xx | Bounded retry with exponential backoff and jitter |
| Network timeout | Retry only after considering duplicate-generation risk |
A 429 can represent either a request-rate limit or a concurrent-generation
limit — the structured error code is what distinguishes them, not the status
code alone.
Retry ambiguity. Audio-generation calls are not inherently equivalent to idempotent database reads. When a connection times out after the server may already have begun or completed generation, an automatic retry can produce a second billable result. That’s an engineering inference rather than a stated ElevenLabs idempotency guarantee, so a robust application should:
- use its own operation identifier;
- record request state;
- limit retry count;
- distinguish connection failure before sending from timeout after sending;
- cache completed output where appropriate;
- avoid stacking application retries on top of undocumented SDK retries.
Observability fields. Record at minimum:
internal_trace_id
elevenlabs_request_id
sdk_package
sdk_version
operation
model_id
voice_id or agent_id
transport
output_format
input_character_count
started_at
time_to_first_byte_or_audio
completed_at
status_code
error_type
retry_countAvoid recording full text or transcripts by default when they can contain personal or confidential information — and, as above, never log API keys or raw audio.