ElevenLabs SDK Patterns
Overview
Production-ready patterns for the ElevenLabs TypeScript and Python SDKs. Covers singleton clients, type-safe wrappers, error handling, retry logic, and multi-tenant patterns.
Prerequisites
@elevenlabs/elevenlabs-jsinstalled (TypeScript) orelevenlabs(Python)- Familiarity with async/await patterns
- Understanding of error handling best practices
Instructions
Pattern 1: Singleton Client with Config
// src/elevenlabs/client.ts
import { ElevenLabsClient } from "@elevenlabs/elevenlabs-js";
let instance: ElevenLabsClient | null = null;
export function getClient(): ElevenLabsClient {
if (!instance) {
if (!process.env.ELEVENLABS_API_KEY) {
throw new Error("ELEVENLABS_API_KEY environment variable is required");
}
instance = new ElevenLabsClient({
apiKey: process.env.ELEVENLABS_API_KEY,
maxRetries: 3, // Auto-retry on 429/5xx (default: 2)
timeoutInSeconds: 60, // Per-request timeout
});
}
return instance;
}
// Reset for testing
export function resetClient(): void {
instance = null;
}
Pattern 2: Type-Safe TTS Service
// src/elevenlabs/tts-service.ts
import { getClient } from "./client";
import { Readable } from "stream";
import { pipeline } from "stream/promises";
import { createWriteStream } from "fs";
export type VoicePreset = "narration" | "conversational" | "dramatic" | "neutral";
const VOICE_PRESETS: Record<VoicePreset, {
stability: number;
similarity_boost: number;
style: number;
}> = {
narration: { stability: 0.6, similarity_boost: 0.75, style: 0.0 },
conversational: { stability: 0.4, similarity_boost: 0.6, style: 0.3 },
dramatic: { stability: 0.3, similarity_boost: 0.8, style: 0.7 },
neutral: { stability: 0.8, similarity_boost: 0.5, style: 0.0 },
};
export interface TTSOptions {
voiceId: string;
text: string;
modelId?: "eleven_v3" | "eleven_multilingual_v2" | "eleven_flash_v2_5" | "eleven_turbo_v2_5";
preset?: VoicePreset;
outputFormat?: string;
languageCode?: string;
}
export async function generateSpeech(options: TTSOptions): Promise<ReadableStream> {
const client = getClient();
const settings = VOICE_PRESETS[options.preset || "narration"];
return client.textToSpeech.convert(options.voiceId, {
text: options.text,
model_id: options.modelId || "eleven_multilingual_v2",
voice_settings: settings,
output_format: options.outputFormat || "mp3_44100_128",
language_code: options.languageCode,
});
}
export async function generateToFile(options: TTSOptions, outputPath: string): Promise<void> {
const audio = await generateSpeech(options);
await pipeline(
Readable.fromWeb(audio as any),
createWriteStream(outputPath)
);
}
Pattern 3: Error Classification
// src/elevenlabs/errors.ts
export type ElevenLabsErrorCode =
| "auth_failed"
| "quota_exceeded"
| "rate_limited"
| "concurrent_limit"
| "voice_not_found"
| "invalid_request"
| "server_error"
| "network_error";
export class ElevenLabsServiceError extends Error {
constructor(
message: string,
public readonly code: ElevenLabsErrorCode,
public readonly httpStatus: number | null,
public readonly retryable: boolean,
public readonly originalError?: Error
) {
super(message);
this.name = "ElevenLabsServiceError";
}
}
export function classifyError(error: unknown): ElevenLabsServiceError {
if (error instanceof Error) {
const status = (error as any).statusCode || (error as any).status;
if (status === 401) {
const msg = (error as any).body?.detail?.message || error.message;
if (msg?.includes("quota")) {
return new ElevenLabsServiceError(msg, "quota_exceeded", 401, false, error);
}
return new ElevenLabsServiceError(msg, "auth_failed", 401, false, error);
}
if (status === 429) {
const msg = (error as any).body?.detail?.message || error.message;
const code = msg?.includes("concurrent") ? "concurrent_limit" : "rate_limited";
return new ElevenLabsServiceError(msg, code, 429, true, error);
}
if (status === 404) {
return new ElevenLabsServiceError(error.message, "voice_not_found", 404, false, error);
}
if (status === 400) {
return new ElevenLabsServiceError(error.message, "invalid_request", 400, false, error);
}
if (status >= 500) {
return new ElevenLabsServiceError(error.message, "server_error", status, true, error);
}
return new ElevenLabsServiceError(error.message, "network_error", null, true, error);
}
return new ElevenLabsServiceError(String(error), "network_error", null, true);
}
Pattern 4: Retry with Concurrency Queue
// src/elevenlabs/queue.ts
import PQueue from "p-queue";
import { classifyError, ElevenLabsServiceError } from "./errors";
// Concurrency limits by plan:
// Free: 2, Starter: 3, Creator: 5, Pro: 10, Scale: 15
const queue = new PQueue({
concurrency: 5, // Match your plan's concurrent request limit
interval: 1000, // Window in ms
intervalCap: 10, // Max requests per window
});
export async function queuedRequest<T>(
operation: () => Promise<T>,
maxRetries = 3
): Promise<T> {
return queue.add(async () => {
let lastError: ElevenLabsServiceError | undefined;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await operation();
} catch (err) {
lastError = classifyError(err);
if (!lastError.retryable) throw lastError;
const delay = Math.min(1000 * Math.pow(2, attempt), 30000);
const jitter = Math.random() * 500;
await new Promise(r => setTimeout(r, delay + jitter));
}
}
throw lastError;
}) as Promise<T>;
}
Pattern 5: Multi-Tenant Client Factory
// src/elevenlabs/multi-tenant.ts
import { ElevenLabsClient } from "@elevenlabs/elevenlabs-js";
const clients = new Map<string, ElevenLabsClient>();
export function getClientForTenant(tenantId: string, apiKey: string): ElevenLabsClient {
if (!clients.has(tenantId)) {
clients.set(tenantId, new ElevenLabsClient({
apiKey,
maxRetries: 3,
}));
}
return clients.get(tenantId)!;
}
export function removeTenantClient(tenantId: string): void {
clients.delete(tenantId);
}
Pattern 6: Python Async Pattern
# elevenlabs_service.py
import os
import asyncio
from elevenlabs.client import AsyncElevenLabsClient
_client: AsyncElevenLabsClient | None = None
def get_async_client() -> AsyncElevenLabsClient:
global _client
if _client is None:
_client = AsyncElevenLabsClient(
api_key=os.environ["ELEVENLABS_API_KEY"]
)
return _client
async def generate_speech(text: str, voice_id: str, output_path: str):
client = get_async_client()
audio = await client.text_to_speech.convert(
voice_id=voice_id,
text=text,
model_id="eleven_multilingual_v2",
voice_settings={
"stability": 0.5,
"similarity_boost": 0.75,
},
)
with open(output_path, "wb") as f:
async for chunk in audio:
f.write(chunk)
# Usage
asyncio.run(generate_speech("Hello!", "21m00Tcm4TlvDq8ikWAM", "out.mp3"))
Pattern Summary
| Pattern | Use Case | Key Benefit | |---------|----------|-------------| | Singleton client | Single-tenant apps | Memory efficiency, shared retries | | Type-safe TTS | All TTS work | Voice presets, compile-time checks | | Error classification | Production error handling | Actionable error codes | | Concurrency queue | High-throughput apps | Respects plan limits automatically | | Multi-tenant factory | SaaS platforms | Per-customer isolation | | Python async | Python backends | Non-blocking I/O |
Error Handling
| Pattern | Error Type | Benefit |
|---------|-----------|---------|
| classifyError() | All API errors | Maps HTTP to actionable codes |
| queuedRequest() | 429, 5xx | Auto-retry with exponential backoff |
| Singleton guard | Missing env var | Fails fast at startup, not at first call |
Resources
Next Steps
Apply patterns in elevenlabs-core-workflow-a for TTS, or see elevenlabs-rate-limits for advanced throttling.