ElevenLabs Cost Tuning
Overview
Optimize ElevenLabs costs through model selection (Flash = 50% savings), character-efficient text processing, audio caching, and real-time quota monitoring. ElevenLabs bills by character for TTS and by audio minute for STT.
Prerequisites
Instructions
Step 1: Understand the Billing Model
TTS billing (by character):
| Model |
Credits per Character |
10K Chars Cost |
Best For |
eleven_v3 |
1.0 |
10,000 credits |
Maximum quality |
eleven_multilingual_v2 |
1.0 |
10,000 credits |
High quality + multilingual |
eleven_flash_v2_5 |
0.5 |
5,000 credits |
Real-time / budget-conscious |
eleven_turbo_v2_5 |
0.5 |
5,000 credits |
Fast + affordable |
Other feature billing:
| Feature |
Billing Basis |
| Speech-to-Text (Scribe) |
Per audio minute |
| Sound Effects |
Per generation |
| Audio Isolation |
1,000 characters per minute of audio |
| Dubbing |
Per source audio minute |
Plan character limits:
| Plan |
Monthly |
Price |
Cost/1K Chars |
| Free |
10,000 |
$0 |
$0 |
| Starter |
30,000 |
$5 |
$0.17 |
| Creator |
100,000 |
$22 |
$0.22 |
| Pro |
500,000 |
$99 |
$0.20 |
| Scale |
2,000,000 |
$330 |
$0.17 |
Step 2: Model-Based Cost Reduction
The easiest win: use Flash/Turbo models where quality difference is acceptable.
// src/elevenlabs/cost-aware-tts.ts
type ContentType = "greeting" | "notification" | "narration" | "premium";
function selectCostEffectiveModel(contentType: ContentType): {
modelId: string;
costMultiplier: number;
} {
switch (contentType) {
case "greeting":
case "notification":
// Short, functional audio — Flash is fine
return { modelId: "eleven_flash_v2_5", costMultiplier: 0.5 };
case "narration":
// Content creation — quality matters
return { modelId: "eleven_multilingual_v2", costMultiplier: 1.0 };
case "premium":
// Customer-facing, high-value — max quality
return { modelId: "eleven_v3", costMultiplier: 1.0 };
}
}
// Estimate cost before generating
function estimateCharacterCost(text: string, modelId: string): number {
const multiplier = modelId.includes("flash") || modelId.includes("turbo") ? 0.5 : 1.0;
return text.length * multiplier;
}
Step 3: Character-Efficient Text Processing
Reduce character count without losing meaning:
// src/elevenlabs/text-optimizer.ts
/**
* Optimize text for TTS to reduce character count.
* ElevenLabs counts ALL characters including spaces and punctuation.
*/
export function optimizeTextForTTS(text: string): {
optimized: string;
originalLength: number;
savedCharacters: number;
} {
const original = text;
let optimized = text;
// Remove excessive whitespace (spaces count as characters)
optimized = optimized.replace(/\s+/g, " ").trim();
// Remove markdown formatting that doesn't affect speech
optimized = optimized.replace(/[*_~`#]/g, "");
optimized = optimized.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1"); // [link](url) → link
// Remove HTML tags
optimized = optimized.replace(/<[^>]+>/g, "");
// Collapse multiple punctuation
optimized = optimized.replace(/\.{2,}/g, ".");
optimized = optimized.replace(/!{2,}/g, "!");
return {
optimized,
originalLength: original.length,
savedCharacters: original.length - optimized.length,
};
}
Step 4: Real-Time Quota Monitoring
// src/elevenlabs/quota-monitor.ts
import { ElevenLabsClient } from "@elevenlabs/elevenlabs-js";
interface QuotaStatus {
plan: string;
used: number;
limit: number;
remaining: number;
pctUsed: number;
resetsAt: Date;
daysUntilReset: number;
dailyBudget: number; // Characters per day until reset
projectedOverage: boolean;
}
export async function getQuotaStatus(): Promise<QuotaStatus> {
const client = new ElevenLabsClient();
const user = await client.user.get();
const sub = user.subscription;
const now = new Date();
const resetsAt = new Date(sub.next_character_count_reset_unix * 1000);
const msUntilReset = resetsAt.getTime() - now.getTime();
const daysUntilReset = Math.max(1, Math.ceil(msUntilReset / 86_400_000));
const remaining = sub.character_limit - sub.character_count;
const dailyBudget = Math.floor(remaining / daysUntilReset);
// Project if current usage rate will exceed limit
const daysSinceReset = 30 - daysUntilReset;
const dailyRate = daysSinceReset > 0 ? sub.character_count / daysSinceReset : 0;
const projectedUsage = dailyRate * 30;
return {
plan: sub.tier,
used: sub.character_count,
limit: sub.character_limit,
remaining,
pctUsed: Math.round((sub.character_count / sub.character_limit) * 1000) / 10,
resetsAt,
daysUntilReset,
dailyBudget,
projectedOverage: projectedUsage > sub.character_limit,
};
}
// CLI usage
async function printQuota() {
const q = await getQuotaStatus();
console.log(`Plan: ${q.plan}`);
console.log(`Used: ${q.used.toLocaleString()} / ${q.limit.toLocaleString()} (${q.pctUsed}%)`);
console.log(`Remaining: ${q.remaining.toLocaleString()} characters`);
console.log(`Daily budget: ${q.dailyBudget.toLocaleString()} chars/day`);
console.log(`Resets: ${q.resetsAt.toISOString()} (${q.daysUntilReset} days)`);
if (q.projectedOverage) {
console.warn("WARNING: Projected to exceed quota at current usage rate");
}
}
Step 5: Cost-Aware Request Guard
// Prevent expensive operations when quota is low
export async function guardedTTS(
text: string,
voiceId: string,
contentType: ContentType = "notification"
): Promise<ReadableStream | null> {
const client = new ElevenLabsClient();
const { modelId, costMultiplier } = selectCostEffectiveModel(contentType);
const charCost = text.length * costMultiplier;
// Check quota
const quota = await getQuotaStatus();
if (charCost > quota.remaining) {
console.error(`Insufficient quota: need ${charCost}, have ${quota.remaining}`);
return null;
}
if (quota.pctUsed > 90) {
// Force cheapest model when quota is critically low
console.warn("Low quota — forcing Flash model");
return client.textToSpeech.convert(voiceId, {
text,
model_id: "eleven_flash_v2_5",
});
}
return client.textToSpeech.convert(voiceId, {
text,
model_id: modelId,
});
}
Step 6: Usage Tracking Dashboard
// Track per-request costs for analysis
interface UsageEntry {
timestamp: Date;
operation: "tts" | "sts" | "stt" | "sfx" | "isolation";
modelId: string;
characterCount: number;
creditCost: number;
voiceId: string;
cached: boolean;
}
const usageLog: UsageEntry[] = [];
function trackUsage(entry: Omit<UsageEntry, "timestamp">) {
usageLog.push({ ...entry, timestamp: new Date() });
}
function getUsageSummary(days = 30) {
const cutoff = new Date(Date.now() - days * 86_400_000);
const recent = usageLog.filter(e => e.timestamp > cutoff);
return {
totalCredits: recent.reduce((sum, e) => sum + e.creditCost, 0),
totalCharacters: recent.reduce((sum, e) => sum + e.characterCount, 0),
byModel: Object.groupBy(recent, e => e.modelId),
byOperation: Object.groupBy(recent, e => e.operation),
cacheHitRate: recent.filter(e => e.cached).length / recent.length,
};
}
Cost Optimization Checklist
| Strategy |
Savings |
Effort |
| Flash/Turbo models for non-premium content |
50% |
Low |
| Cache repeated audio (greetings, prompts) |
80-95% for cached |
Medium |
| Text optimization (remove markdown, whitespace) |
5-15% |
Low |
| Quota monitoring with budget alerts |
Prevents overages |
Medium |
| Usage-based billing (Creator+ plans) |
Avoids hard cutoff |
Low |
| Batch short texts into single requests |
Reduces overhead |
Low |
Error Handling
| Issue |
Cause |
Solution |
quota_exceeded (401) |
Monthly limit hit |
Upgrade plan or enable usage-based billing |
| Unexpected high usage |
No monitoring |
Implement getQuotaStatus() guard |
| Bill shock |
Wrong model in production |
Audit model_id in all TTS calls |
| Cache not helping |
Unique content |
Cache only repeated content (greetings, errors) |
Resources
Next Steps
For architecture patterns, see elevenlabs-reference-architecture.
1---2name: elevenlabs-cost-tuning3description: |4 Optimize ElevenLabs costs through model selection, character-efficient patterns,5 caching, and usage monitoring with budget alerts.6 Use when analyzing ElevenLabs billing, reducing character usage,7 or implementing quota monitoring for TTS workloads.8 Trigger: "elevenlabs cost", "elevenlabs billing", "reduce elevenlabs costs",9 "elevenlabs pricing", "elevenlabs expensive", "elevenlabs budget",10 "elevenlabs characters", "elevenlabs quota".11allowed-tools: Read, Grep, Bash(curl:*), Bash(node:*)12version: 1.0.013license: MIT14author: Jeremy Longshore <jeremy@intentsolutions.io>15tags: [saas, voice, ai, elevenlabs, cost, billing]16compatible-with: claude-code17---1819# ElevenLabs Cost Tuning2021## Overview2223Optimize ElevenLabs costs through model selection (Flash = 50% savings), character-efficient text processing, audio caching, and real-time quota monitoring. ElevenLabs bills by character for TTS and by audio minute for STT.2425## Prerequisites2627- ElevenLabs account with usage dashboard access28- Understanding of your monthly character consumption29- Access to billing at https://elevenlabs.io/app/subscription3031## Instructions3233### Step 1: Understand the Billing Model3435**TTS billing (by character):**3637| Model | Credits per Character | 10K Chars Cost | Best For |38|-------|-----------------------|----------------|----------|39| `eleven_v3` | 1.0 | 10,000 credits | Maximum quality |40| `eleven_multilingual_v2` | 1.0 | 10,000 credits | High quality + multilingual |41| `eleven_flash_v2_5` | 0.5 | 5,000 credits | Real-time / budget-conscious |42| `eleven_turbo_v2_5` | 0.5 | 5,000 credits | Fast + affordable |4344**Other feature billing:**4546| Feature | Billing Basis |47|---------|--------------|48| Speech-to-Text (Scribe) | Per audio minute |49| Sound Effects | Per generation |50| Audio Isolation | 1,000 characters per minute of audio |51| Dubbing | Per source audio minute |5253**Plan character limits:**5455| Plan | Monthly | Price | Cost/1K Chars |56|------|---------|-------|---------------|57| Free | 10,000 | $0 | $0 |58| Starter | 30,000 | $5 | $0.17 |59| Creator | 100,000 | $22 | $0.22 |60| Pro | 500,000 | $99 | $0.20 |61| Scale | 2,000,000 | $330 | $0.17 |6263### Step 2: Model-Based Cost Reduction6465The easiest win: use Flash/Turbo models where quality difference is acceptable.6667```typescript68// src/elevenlabs/cost-aware-tts.ts69type ContentType = "greeting" | "notification" | "narration" | "premium";7071function selectCostEffectiveModel(contentType: ContentType): {72 modelId: string;73 costMultiplier: number;74} {75 switch (contentType) {76 case "greeting":77 case "notification":78 // Short, functional audio — Flash is fine79 return { modelId: "eleven_flash_v2_5", costMultiplier: 0.5 };8081 case "narration":82 // Content creation — quality matters83 return { modelId: "eleven_multilingual_v2", costMultiplier: 1.0 };8485 case "premium":86 // Customer-facing, high-value — max quality87 return { modelId: "eleven_v3", costMultiplier: 1.0 };88 }89}9091// Estimate cost before generating92function estimateCharacterCost(text: string, modelId: string): number {93 const multiplier = modelId.includes("flash") || modelId.includes("turbo") ? 0.5 : 1.0;94 return text.length * multiplier;95}96```9798### Step 3: Character-Efficient Text Processing99100Reduce character count without losing meaning:101102```typescript103// src/elevenlabs/text-optimizer.ts104105/**106 * Optimize text for TTS to reduce character count.107 * ElevenLabs counts ALL characters including spaces and punctuation.108 */109export function optimizeTextForTTS(text: string): {110 optimized: string;111 originalLength: number;112 savedCharacters: number;113} {114 const original = text;115 let optimized = text;116117 // Remove excessive whitespace (spaces count as characters)118 optimized = optimized.replace(/\s+/g, " ").trim();119120 // Remove markdown formatting that doesn't affect speech121 optimized = optimized.replace(/[*_~`#]/g, "");122 optimized = optimized.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1"); // [link](url) → link123124 // Remove HTML tags125 optimized = optimized.replace(/<[^>]+>/g, "");126127 // Collapse multiple punctuation128 optimized = optimized.replace(/\.{2,}/g, ".");129 optimized = optimized.replace(/!{2,}/g, "!");130131 return {132 optimized,133 originalLength: original.length,134 savedCharacters: original.length - optimized.length,135 };136}137```138139### Step 4: Real-Time Quota Monitoring140141```typescript142// src/elevenlabs/quota-monitor.ts143import { ElevenLabsClient } from "@elevenlabs/elevenlabs-js";144145interface QuotaStatus {146 plan: string;147 used: number;148 limit: number;149 remaining: number;150 pctUsed: number;151 resetsAt: Date;152 daysUntilReset: number;153 dailyBudget: number; // Characters per day until reset154 projectedOverage: boolean;155}156157export async function getQuotaStatus(): Promise<QuotaStatus> {158 const client = new ElevenLabsClient();159 const user = await client.user.get();160 const sub = user.subscription;161162 const now = new Date();163 const resetsAt = new Date(sub.next_character_count_reset_unix * 1000);164 const msUntilReset = resetsAt.getTime() - now.getTime();165 const daysUntilReset = Math.max(1, Math.ceil(msUntilReset / 86_400_000));166167 const remaining = sub.character_limit - sub.character_count;168 const dailyBudget = Math.floor(remaining / daysUntilReset);169170 // Project if current usage rate will exceed limit171 const daysSinceReset = 30 - daysUntilReset;172 const dailyRate = daysSinceReset > 0 ? sub.character_count / daysSinceReset : 0;173 const projectedUsage = dailyRate * 30;174175 return {176 plan: sub.tier,177 used: sub.character_count,178 limit: sub.character_limit,179 remaining,180 pctUsed: Math.round((sub.character_count / sub.character_limit) * 1000) / 10,181 resetsAt,182 daysUntilReset,183 dailyBudget,184 projectedOverage: projectedUsage > sub.character_limit,185 };186}187188// CLI usage189async function printQuota() {190 const q = await getQuotaStatus();191 console.log(`Plan: ${q.plan}`);192 console.log(`Used: ${q.used.toLocaleString()} / ${q.limit.toLocaleString()} (${q.pctUsed}%)`);193 console.log(`Remaining: ${q.remaining.toLocaleString()} characters`);194 console.log(`Daily budget: ${q.dailyBudget.toLocaleString()} chars/day`);195 console.log(`Resets: ${q.resetsAt.toISOString()} (${q.daysUntilReset} days)`);196 if (q.projectedOverage) {197 console.warn("WARNING: Projected to exceed quota at current usage rate");198 }199}200```201202### Step 5: Cost-Aware Request Guard203204```typescript205// Prevent expensive operations when quota is low206export async function guardedTTS(207 text: string,208 voiceId: string,209 contentType: ContentType = "notification"210): Promise<ReadableStream | null> {211 const client = new ElevenLabsClient();212 const { modelId, costMultiplier } = selectCostEffectiveModel(contentType);213 const charCost = text.length * costMultiplier;214215 // Check quota216 const quota = await getQuotaStatus();217218 if (charCost > quota.remaining) {219 console.error(`Insufficient quota: need ${charCost}, have ${quota.remaining}`);220 return null;221 }222223 if (quota.pctUsed > 90) {224 // Force cheapest model when quota is critically low225 console.warn("Low quota — forcing Flash model");226 return client.textToSpeech.convert(voiceId, {227 text,228 model_id: "eleven_flash_v2_5",229 });230 }231232 return client.textToSpeech.convert(voiceId, {233 text,234 model_id: modelId,235 });236}237```238239### Step 6: Usage Tracking Dashboard240241```typescript242// Track per-request costs for analysis243interface UsageEntry {244 timestamp: Date;245 operation: "tts" | "sts" | "stt" | "sfx" | "isolation";246 modelId: string;247 characterCount: number;248 creditCost: number;249 voiceId: string;250 cached: boolean;251}252253const usageLog: UsageEntry[] = [];254255function trackUsage(entry: Omit<UsageEntry, "timestamp">) {256 usageLog.push({ ...entry, timestamp: new Date() });257}258259function getUsageSummary(days = 30) {260 const cutoff = new Date(Date.now() - days * 86_400_000);261 const recent = usageLog.filter(e => e.timestamp > cutoff);262263 return {264 totalCredits: recent.reduce((sum, e) => sum + e.creditCost, 0),265 totalCharacters: recent.reduce((sum, e) => sum + e.characterCount, 0),266 byModel: Object.groupBy(recent, e => e.modelId),267 byOperation: Object.groupBy(recent, e => e.operation),268 cacheHitRate: recent.filter(e => e.cached).length / recent.length,269 };270}271```272273## Cost Optimization Checklist274275| Strategy | Savings | Effort |276|----------|---------|--------|277| Flash/Turbo models for non-premium content | 50% | Low |278| Cache repeated audio (greetings, prompts) | 80-95% for cached | Medium |279| Text optimization (remove markdown, whitespace) | 5-15% | Low |280| Quota monitoring with budget alerts | Prevents overages | Medium |281| Usage-based billing (Creator+ plans) | Avoids hard cutoff | Low |282| Batch short texts into single requests | Reduces overhead | Low |283284## Error Handling285286| Issue | Cause | Solution |287|-------|-------|----------|288| `quota_exceeded` (401) | Monthly limit hit | Upgrade plan or enable usage-based billing |289| Unexpected high usage | No monitoring | Implement `getQuotaStatus()` guard |290| Bill shock | Wrong model in production | Audit `model_id` in all TTS calls |291| Cache not helping | Unique content | Cache only repeated content (greetings, errors) |292293## Resources294295- [ElevenLabs Pricing](https://elevenlabs.io/pricing)296- [Usage Dashboard](https://elevenlabs.io/app/usage)297- [Subscription Settings](https://elevenlabs.io/app/subscription)298- [Models Overview](https://elevenlabs.io/docs/overview/models)299300## Next Steps301302For architecture patterns, see `elevenlabs-reference-architecture`.303