This guide covers common issues when working with the ElevenLabs integration and their solutions.
Table of Contents#
- Authentication Issues
- Rate Limiting
- Quota and Usage Issues
- Voice Issues
- Audio Quality Issues
- Streaming Issues
- Voice Cloning Issues
- Model-Specific Issues
- Network and Connectivity
- Performance Issues
- Debugging Tools
Authentication Issues#
Error: "Invalid API Key" (401)#
Symptoms:
ElevenLabsProviderErrorwith codeINVALID_API_KEY- HTTP 401 Unauthorized response
Causes:
- Missing API key
- Invalid or expired API key
- API key with insufficient permissions
Solutions:
typescript
// 1. Verify API key is set
if (!process.env.ELEVENLABS_API_KEY) {
throw new Error('ELEVENLABS_API_KEY environment variable not set');
}
// 2. Test API key validity
try {
const provider = new ElevenLabsProvider({
apiKey: process.env.ELEVENLABS_API_KEY,
});
await provider.getUsage(); // Simple test call
console.log('API key is valid');
} catch (error) {
if (error.code === 'INVALID_API_KEY') {
console.error('API key is invalid or expired');
// Regenerate at: https://elevenlabs.io/api
}
}
Verification steps:
- Log into ElevenLabs dashboard
- Navigate to Profile > API Key
- Regenerate key if necessary
- Update
ELEVENLABS_API_KEYin your environment
Error: "Unauthorized" for Voice Cloning#
Symptoms:
- Can synthesize but cannot clone voices
- 403 Forbidden on clone endpoints
Cause: Voice cloning requires paid subscription tier
Solution:
typescript
// Check subscription level
const subscription = await provider.getSubscription();
if (subscription.tier === 'free') {
console.error('Voice cloning requires paid subscription');
console.log('Upgrade at: https://elevenlabs.io/subscription');
}
Rate Limiting#
Error: "Rate Limited" (429)#
Symptoms:
ElevenLabsProviderErrorwith codeRATE_LIMITED- HTTP 429 Too Many Requests
- Requests fail in bursts
Rate Limits by Tier:
| Tier | Requests/min | Concurrent |
|---|---|---|
| Free | 20 | 2 |
| Starter | 100 | 5 |
| Creator | 500 | 10 |
| Pro | 1000 | 20 |
| Enterprise | Custom | Custom |
Solutions:
typescript
// 1. Built-in retry with exponential backoff
const provider = new ElevenLabsProvider({
apiKey: process.env.ELEVENLABS_API_KEY,
maxRetries: 5,
retryDelayMs: 1000, // Base delay
});
// 2. Manual rate limiting with queue
import pLimit from 'p-limit';
const limit = pLimit(5); // 5 concurrent requests
const texts = ['text1', 'text2', 'text3' /* ... */];
const results = await Promise.all(
texts.map((text) =>
limit(() =>
provider.synthesize({
text,
voice_id: 'voice_id',
})
)
)
);
// 3. Add delays between requests
async function synthesizeWithDelay(
texts: string[],
delayMs: number = 200
): Promise<Buffer[]> {
const results: Buffer[] = [];
for (const text of texts) {
const response = await provider.synthesize({
text,
voice_id: 'voice_id',
});
results.push(response.audio);
await new Promise((resolve) => setTimeout(resolve, delayMs));
}
return results;
}
Handling Rate Limit Headers#
typescript
// The provider automatically handles Retry-After headers
// For custom handling:
try {
await provider.synthesize({ text, voice_id });
} catch (error) {
if (error.code === 'RATE_LIMITED') {
const retryAfter = error.retryAfter || 60; // seconds
console.log(`Rate limited. Retry after ${retryAfter}s`);
await new Promise((r) => setTimeout(r, retryAfter * 1000));
// Retry the request
}
}
Quota and Usage Issues#
Error: "Quota Exceeded"#
Symptoms:
ElevenLabsProviderErrorwith codeQUOTA_EXCEEDED- Cannot synthesize despite valid API key
Diagnosis:
typescript
// Check current usage
const usage = await provider.getUsage();
console.log(`Characters used: ${usage.character_count}`);
console.log(`Character limit: ${usage.character_limit}`);
console.log(`Remaining: ${usage.character_limit - usage.character_count}`);
console.log(`Reset date: ${usage.next_reset_date}`);
if (usage.character_count >= usage.character_limit) {
console.error('Quota exceeded');
if (usage.can_extend_character_limit) {
console.log('You can extend your limit in the dashboard');
}
}
Solutions:
- Wait for monthly reset
- Upgrade subscription tier
- Purchase character add-on
- Optimize text to reduce character count
Optimizing Character Usage#
typescript
// 1. Remove unnecessary whitespace
const optimizedText = text.replace(/\s+/g, ' ').trim();
// 2. Use abbreviations for common words
const abbreviations: Record<string, string> = {
'for example': 'e.g.',
'that is': 'i.e.',
approximately: 'approx.',
};
// 3. Estimate cost before synthesis
function estimateCharacterCost(text: string): number {
// ElevenLabs counts all characters including spaces
return text.length;
}
const cost = estimateCharacterCost(text);
const remaining = usage.character_limit - usage.character_count;
if (cost > remaining) {
throw new Error(`Text too long: ${cost} chars, only ${remaining} remaining`);
}
Voice Issues#
Error: "Voice Not Found"#
Symptoms:
ElevenLabsProviderErrorwith codeVOICE_NOT_FOUND- HTTP 404 on synthesis request
Causes:
- Invalid voice ID
- Voice was deleted
- Voice belongs to different account
- Cloned voice not yet processed
Solutions:
typescript
// 1. Validate voice ID before use
async function validateVoiceId(voiceId: string): Promise<boolean> {
try {
await provider.getVoice(voiceId);
return true;
} catch (error) {
if (error.code === 'VOICE_NOT_FOUND') {
console.error(`Voice ${voiceId} not found`);
return false;
}
throw error;
}
}
// 2. List available voices
const voices = await provider.listVoices();
const voiceIds = voices.map((v) => v.voice_id);
if (!voiceIds.includes(targetVoiceId)) {
console.error('Voice not in available list');
console.log(
'Available voices:',
voices.map((v) => `${v.name}: ${v.voice_id}`)
);
}
// 3. Use fallback voice
const FALLBACK_VOICE_ID = 'EXAVITQu4vr4xnSDxMaL'; // Sarah
async function synthesizeWithFallback(text: string, voiceId: string) {
try {
return await provider.synthesize({ text, voice_id: voiceId });
} catch (error) {
if (error.code === 'VOICE_NOT_FOUND') {
console.warn(`Voice ${voiceId} not found, using fallback`);
return await provider.synthesize({ text, voice_id: FALLBACK_VOICE_ID });
}
throw error;
}
}
Voice Sounds Different Than Expected#
Symptoms:
- Voice output doesn't match preview
- Voice quality inconsistent
Causes:
- Different model being used
- Voice settings not applied
- Text context affecting delivery
Solutions:
typescript
// 1. Ensure consistent model
const response = await provider.synthesize({
text,
voice_id: 'voice_id',
model_id: 'eleven_v3', // Explicitly set model
});
// 2. Apply optimal voice settings
const response = await provider.synthesize({
text,
voice_id: 'voice_id',
voice_settings: {
stability: 0.75, // Consistent delivery
similarity_boost: 0.85, // Match original voice
style: 0.0, // Neutral style
use_speaker_boost: true,
},
});
// 3. Provide context for better intonation
const response = await provider.synthesize({
text: 'Current sentence to speak.',
voice_id: 'voice_id',
previous_text: 'Context from before.', // Helps with intonation
next_text: 'What comes after.', // Helps with pacing
});
Audio Quality Issues#
Audio Sounds Robotic or Unnatural#
Causes:
- Using legacy model
- Inappropriate voice settings
- Text formatting issues
Solutions:
typescript
// 1. Use latest model for best quality
const response = await provider.synthesize({
text,
voice_id: 'voice_id',
model_id: 'eleven_v3', // Most natural sounding
});
// 2. Adjust stability for more natural variation
voice_settings: {
stability: 0.5, // Lower = more variation
similarity_boost: 0.7,
style: 0.3 // Some expressiveness
}
// 3. Add natural pauses with audio tags
const naturalText = text
.replace(/\. /g, '. <break time="0.3s" /> ')
.replace(/\? /g, '? <break time="0.4s" /> ')
.replace(/, /g, ', <break time="0.1s" /> ');
Audio Has Artifacts or Glitches#
Causes:
- Text contains problematic characters
- Very long text without breaks
- Special characters not handled
Solutions:
typescript
// 1. Clean text before synthesis
function cleanTextForSynthesis(text: string): string {
return (
text
// Remove control characters
.replace(/[\x00-\x1F\x7F]/g, '')
// Normalize quotes
.replace(/[""]/g, '"')
.replace(/['']/g, "'")
// Remove emojis (unless supported)
.replace(
/[\u{1F600}-\u{1F64F}\u{1F300}-\u{1F5FF}\u{1F680}-\u{1F6FF}\u{1F1E0}-\u{1F1FF}]/gu,
''
)
// Normalize whitespace
.replace(/\s+/g, ' ')
.trim()
);
}
// 2. Split long text at natural boundaries
function splitAtSentences(text: string, maxLength: number = 500): string[] {
const sentences = text.match(/[^.!?]+[.!?]+/g) || [text];
const chunks: string[] = [];
let current = '';
for (const sentence of sentences) {
if ((current + sentence).length > maxLength) {
if (current) chunks.push(current.trim());
current = sentence;
} else {
current += sentence;
}
}
if (current) chunks.push(current.trim());
return chunks;
}
Wrong Output Format#
Symptoms:
- File won't play
- Wrong sample rate
- Unexpected file size
Solutions:
typescript
// Explicitly specify format
const response = await provider.synthesize({
text,
voice_id: 'voice_id',
output_format: 'mp3_44100_128', // Clear format specification
});
// Verify output
const audioBuffer = response.audio;
console.log(`Output size: ${audioBuffer.length} bytes`);
console.log(`Expected MP3 header: ${audioBuffer.slice(0, 3).toString()}`);
// MP3 should start with 'ID3' or 0xFF 0xFB
Streaming Issues#
WebSocket Connection Fails#
Symptoms:
- Cannot establish streaming connection
- Connection drops immediately
- Timeout errors
Causes:
- Network/firewall blocking WebSocket
- Invalid configuration
- Server temporarily unavailable
Solutions:
typescript
// 1. Verify WebSocket URL
const provider = new ElevenLabsProvider({
apiKey: process.env.ELEVENLABS_API_KEY,
websocketUrl: 'wss://api.elevenlabs.io/v1/text-to-speech',
websocketTimeoutSeconds: 30, // Increase timeout
});
// 2. Add connection error handling
provider.on('streaming-error', (event) => {
console.error(`Stream error: ${event.error}`);
if (event.error.includes('connection')) {
// Network issue - check proxy settings
console.log('Check firewall/proxy for WebSocket support');
}
});
// 3. Implement reconnection logic
async function streamWithReconnect(
text: string,
maxRetries: number = 3
): Promise<void> {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const session = await provider.startStreaming({
voice_id: 'voice_id',
model_id: 'eleven_flash_v2_5',
});
await provider.sendStreamingText(session.sessionId, text);
return;
} catch (error) {
console.error(`Stream attempt ${attempt + 1} failed:`, error);
if (attempt < maxRetries - 1) {
await new Promise((r) => setTimeout(r, 1000 * (attempt + 1)));
}
}
}
throw new Error('Failed to establish stream after retries');
}
Stream Audio Choppy or Delayed#
Causes:
- Network latency
- Buffer underrun
- Wrong model for real-time use
Solutions:
typescript
// 1. Use Flash model for lowest latency
const session = await provider.startStreaming({
voice_id: 'voice_id',
model_id: 'eleven_flash_v2_5', // ~75ms latency
});
// 2. Optimize latency settings
const session = await provider.startStreaming({
voice_id: 'voice_id',
model_id: 'eleven_flash_v2_5',
optimize_streaming_latency: 4, // Maximum latency optimization
});
// 3. Pre-buffer audio chunks
const audioBuffer: Buffer[] = [];
const MIN_BUFFER_SIZE = 3; // Wait for 3 chunks before playing
provider.on('streaming-audio', (event) => {
audioBuffer.push(event.audio);
if (audioBuffer.length >= MIN_BUFFER_SIZE) {
// Start playing
playNextChunk();
}
});
Voice Cloning Issues#
Clone Request Fails#
Symptoms:
- 400 Bad Request on clone
- "Invalid audio file" error
Causes:
- Audio file too short/long
- Wrong audio format
- Poor audio quality
Requirements:
- Minimum duration: 30 seconds per file
- Maximum files: 25 for instant clone
- Formats: MP3, WAV, M4A
- Quality: Clear speech, minimal background noise
Solutions:
typescript
// 1. Validate audio files before upload
import { parseBuffer } from 'music-metadata';
async function validateCloneAudio(buffer: Buffer): Promise<void> {
const metadata = await parseBuffer(buffer);
// Check duration
if (metadata.format.duration && metadata.format.duration < 30) {
throw new Error('Audio must be at least 30 seconds');
}
// Check format
const validFormats = ['mp3', 'wav', 'm4a', 'mpeg'];
if (!validFormats.includes(metadata.format.container?.toLowerCase() || '')) {
throw new Error(`Invalid format. Use: ${validFormats.join(', ')}`);
}
// Check for mono/stereo
if (
metadata.format.numberOfChannels &&
metadata.format.numberOfChannels > 2
) {
throw new Error('Audio must be mono or stereo');
}
}
// 2. Normalize audio before upload
// Use ffmpeg to standardize format
// ffmpeg -i input.wav -ar 44100 -ac 1 -b:a 128k output.mp3
Cloned Voice Sounds Different#
Causes:
- Insufficient training data
- Background noise in samples
- Multiple speakers in samples
Solutions:
typescript
// 1. Use more/better samples for professional clone
const clonedVoice = await provider.cloneVoiceProfessional({
name: 'Custom Voice',
files: highQualitySamples, // 30+ minutes recommended
description: 'Clear speech with consistent tone',
remove_background_noise: true,
});
// 2. Use optimal settings for cloned voices
const response = await provider.synthesize({
text,
voice_id: clonedVoice.voice_id,
voice_settings: {
stability: 0.85, // Higher for cloned voices
similarity_boost: 0.9, // High to match original
style: 0.0, // Minimal style deviation
},
});
Model-Specific Issues#
Audio Tags Not Working#
Cause: Audio tags only work with eleven_v3 model
Solution:
typescript
// Verify model supports audio tags
const text = 'Hello <break time="1s" /> world';
const response = await provider.synthesize({
text,
voice_id: 'voice_id',
model_id: 'eleven_v3', // Required for audio tags
});
// Check model capabilities
const models = await provider.getModels();
const v3Model = models.find((m) => m.model_id === 'eleven_v3');
console.log('Audio tags supported:', v3Model?.can_use_audio_tags);
Model Not Available#
Symptoms:
- "Model not found" error
- 400 Bad Request with model ID
Solutions:
typescript
// 1. List available models
const models = await provider.getModels();
console.log(
'Available models:',
models.map((m) => m.model_id)
);
// 2. Use model availability check
async function getAvailableModel(
preferred: string,
fallback: string
): Promise<string> {
const models = await provider.getModels();
const modelIds = models.map((m) => m.model_id);
if (modelIds.includes(preferred)) return preferred;
if (modelIds.includes(fallback)) return fallback;
throw new Error(`Neither ${preferred} nor ${fallback} available`);
}
const modelId = await getAvailableModel('eleven_v3', 'eleven_multilingual_v2');
Network and Connectivity#
Timeout Errors#
Symptoms:
- Request timeout after 30 seconds
- Connection reset errors
Solutions:
typescript
// 1. Increase timeout for long text
const provider = new ElevenLabsProvider({
apiKey: process.env.ELEVENLABS_API_KEY,
timeout: 60000, // 60 seconds
});
// 2. Split long text into chunks
const MAX_CHUNK_LENGTH = 2500;
async function synthesizeLongText(text: string): Promise<Buffer> {
const chunks = splitAtSentences(text, MAX_CHUNK_LENGTH);
const audioChunks: Buffer[] = [];
for (const chunk of chunks) {
const response = await provider.synthesize({
text: chunk,
voice_id: 'voice_id',
});
audioChunks.push(response.audio);
}
return Buffer.concat(audioChunks);
}
SSL/TLS Errors#
Symptoms:
- Certificate errors
- Handshake failures
Solutions:
typescript
// For development/debugging only
// NOT recommended for production
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
// Better: Update Node.js CA certificates
// npm install -g ca-certificates
Performance Issues#
Slow Response Times#
Diagnosis:
typescript
// Enable request logging
const provider = new ElevenLabsProvider({
apiKey: process.env.ELEVENLABS_API_KEY,
logRequests: true,
});
// Check statistics
const stats = provider.getStats();
console.log(`Average latency: ${stats.averageLatencyMs}ms`);
console.log(`Total requests: ${stats.totalRequests}`);
console.log(`By model:`, stats.requestsByModel);
Solutions:
typescript
// 1. Use faster models for real-time
model_id: 'eleven_flash_v2_5';
// 2. Optimize latency
optimize_streaming_latency: 4;
// 3. Pre-warm voice cache
await provider.listVoices(); // Cache voices
await provider.getVoice('frequently_used_voice'); // Pre-fetch
// 4. Parallel processing for multiple texts
const results = await Promise.all(
texts.map((text) =>
provider.synthesize({
text,
voice_id: 'voice_id',
model_id: 'eleven_flash_v2_5',
})
)
);
Debugging Tools#
Enable Verbose Logging#
typescript
// Configure provider with logging
const provider = new ElevenLabsProvider({
apiKey: process.env.ELEVENLABS_API_KEY,
logRequests: true,
});
// Listen to all events
provider.on('synthesis-start', (e) => console.log('Start:', e));
provider.on('synthesis-complete', (e) => console.log('Complete:', e));
provider.on('synthesis-error', (e) => console.error('Error:', e));
provider.on('rate-limited', (e) => console.warn('Rate limited:', e));
Health Check Endpoint#
typescript
async function elevenLabsHealthCheck(): Promise<{
status: 'healthy' | 'degraded' | 'down';
details: Record<string, unknown>;
}> {
try {
const start = Date.now();
// Test basic connectivity
const usage = await provider.getUsage();
const apiLatency = Date.now() - start;
// Test voice list
const voicesStart = Date.now();
const voices = await provider.listVoices();
const voiceLatency = Date.now() - voicesStart;
return {
status: 'healthy',
details: {
apiLatencyMs: apiLatency,
voiceLatencyMs: voiceLatency,
voicesAvailable: voices.length,
charactersRemaining: usage.character_limit - usage.character_count,
quotaResetDate: usage.next_reset_date,
},
};
} catch (error) {
return {
status: error.code === 'RATE_LIMITED' ? 'degraded' : 'down',
details: {
error: error.message,
code: error.code,
},
};
}
}
Debug Request/Response#
typescript
// Log raw HTTP for debugging
import { createLogger } from '@oshun/logging';
const logger = createLogger({ service: 'elevenlabs-debug' });
// Wrap provider methods for debugging
const originalSynthesize = provider.synthesize.bind(provider);
provider.synthesize = async (request) => {
logger.debug('Synthesis request', {
text_length: request.text.length,
voice_id: request.voice_id,
model_id: request.model_id,
});
try {
const response = await originalSynthesize(request);
logger.debug('Synthesis response', {
audio_size: response.audio.length,
latency_ms: response.latencyMs,
});
return response;
} catch (error) {
logger.error('Synthesis failed', {
error: error.message,
code: error.code,
});
throw error;
}
};
Getting Help#
Support Resources#
- ElevenLabs Documentation: https://docs.elevenlabs.io
- API Status Page: https://status.elevenlabs.io
- Discord Community: https://discord.gg/elevenlabs
- Support Email: support@elevenlabs.io
Information to Include in Support Requests#
- API key prefix (first 8 characters)
- Error code and message
- Request parameters (without sensitive data)
- Timestamp and timezone
- Subscription tier
- Node.js version
- Provider version
typescript
// Generate debug info for support
async function getDebugInfo(): Promise<string> {
const usage = await provider.getUsage();
const stats = provider.getStats();
return JSON.stringify(
{
nodeVersion: process.version,
timestamp: new Date().toISOString(),
usage: {
charactersUsed: usage.character_count,
limit: usage.character_limit,
},
stats: {
totalRequests: stats.totalRequests,
successRate:
(stats.successfulRequests / stats.totalRequests) * 100 + '%',
avgLatency: stats.averageLatencyMs + 'ms',
},
},
null,
2
);
}