
If you are adding synthesized speech to a product and cannot tell which vendor actually fits, the OpenAI TTS vs ElevenLabs vs Cartesia decision usually comes down to three constraints: how fast the first audio byte arrives, how much the voice needs to sound like a specific human, and what you are willing to pay per million characters. This guide compares all three text-to-speech APIs on those axes, shows working integration code for each, and gives a concrete recommendation per use case. It is written for backend and full-stack engineers who already ship API integrations and now need to pick one without burning a sprint on bake-offs.
The short version: Cartesia wins on conversational latency, ElevenLabs wins on voice fidelity and cloning, and OpenAI wins on convenience if you are already billing through OpenAI. However, the interesting details live in the trade-offs, so keep reading before you commit.
What Each Text-to-Speech API Optimizes For
Every text-to-speech vendor makes the same core trade-off: audio quality, latency, and price cannot all be maximized at once. OpenAI optimizes for integration simplicity and steerability. ElevenLabs optimizes for voice realism, emotional range, and cloning. Cartesia optimizes for time-to-first-audio in real-time conversational systems. Your use case decides which of the three constraints you refuse to compromise on.
That framing matters more than any feature checklist. A podcast generator that renders overnight does not care about a 200ms difference. Conversely, a phone agent that answers support calls lives or dies on that same 200ms, because human callers start talking over dead air after roughly a second. Therefore, identify your latency budget first, then compare.
OpenAI TTS vs ElevenLabs vs Cartesia: Feature Comparison
| Feature | OpenAI TTS | ElevenLabs | Cartesia |
|---|---|---|---|
| Flagship model | gpt-4o-mini-tts | eleven_v3 | sonic-3.5 |
| Low-latency model | tts-1 | eleven_flash_v2_5 | sonic-3.5 |
| Stated model latency | Not published | ~75ms for Flash v2.5 | Not published |
| Languages | Multilingual, count not published | 29 (v2), 70+ (v3), 32 (Flash) | 40+ |
| Voice cloning | No | Yes (instant and professional) | Yes |
| WebSocket streaming | No (chunked HTTP only) | Yes | Yes |
| µ-law 8kHz output | Not listed | Yes | Yes |
| Prompt-style voice direction | Yes (instructions) | Partial (voice_settings) | Via voice selection |
| Data residency endpoints | No | Yes (US, EU, India, Singapore) | No |
| Listed price per 1K chars | $0.015 (tts-1) | $0.10 (v2/v3), $0.05 (Flash) | See cost section |
Two rows in that table drive most real decisions. First, WebSocket streaming: OpenAI’s speech endpoint streams over chunked HTTP transfer encoding, which is fine for a “read this paragraph aloud” button but awkward when an LLM is producing text token by token. Second, µ-law: if you are piping audio into Twilio or any SIP trunk, you need 8kHz µ-law, and only two of the three hand it to you directly.
OpenAI TTS: Steerable Voice Inside an Existing Stack
OpenAI exposes speech synthesis at POST /v1/audio/speech with three models: gpt-4o-mini-tts, tts-1, and tts-1-hd. The newer gpt-4o-mini-tts accepts an instructions parameter, which is the genuinely differentiated feature here. Instead of tuning numeric sliders, you describe the delivery in plain English.
# pip install openai
from openai import OpenAI
from pathlib import Path
client = OpenAI() # reads OPENAI_API_KEY from the environment
def synthesize(text: str, out_path: Path) -> Path:
"""Render text to MP3 using OpenAI's steerable TTS model."""
with client.audio.speech.with_streaming_response.create(
model="gpt-4o-mini-tts",
voice="cedar",
input=text,
# Plain-language delivery direction. This is the main reason to
# pick gpt-4o-mini-tts over the older tts-1 models.
instructions=(
"Speak like a calm support agent. Moderate pace, "
"warm but not chirpy. Pause briefly after each sentence."
),
response_format="mp3",
) as response:
response.stream_to_file(out_path)
return out_path
synthesize("Your order shipped this morning.", Path("reply.mp3"))
Why this works: with_streaming_response writes chunks to disk as they arrive rather than buffering the whole file in memory, which matters once you are rendering paragraphs instead of sentences. The instructions field steers prosody without requiring you to hand-tune stability or similarity values.
The available voices for gpt-4o-mini-tts include alloy, ash, ballad, coral, echo, fable, nova, onyx, sage, shimmer, verse, marin, and cedar. OpenAI’s text-to-speech guide currently recommends marin or cedar for the best quality. Notably, there is no voice cloning, so you cannot ship a specific person’s voice.
For the lowest latency, request wav or pcm instead of mp3. Encoding costs time, and raw PCM skips it entirely.
ElevenLabs: Voice Fidelity and Cloning at a Premium
ElevenLabs is the choice when the voice itself is the product. The API is voice-scoped: you call POST /v1/text-to-speech/{voice_id}, and the voice ID references either a stock voice or one you cloned.
# pip install httpx
import os
import httpx
ELEVEN_KEY = os.environ["ELEVENLABS_API_KEY"]
def synthesize_eleven(text: str, voice_id: str, out_path: str) -> None:
"""Stream ElevenLabs audio to disk without buffering the full clip."""
url = f"https://api.elevenlabs.io/v1/text-to-speech/{voice_id}/stream"
payload = {
"text": text,
# Flash is the low-latency family; swap for eleven_multilingual_v2
# when quality matters more than time-to-first-byte.
"model_id": "eleven_flash_v2_5",
"voice_settings": {
"stability": 0.5, # lower = more emotional variance
"similarity_boost": 0.75, # higher = closer to the source voice
"speed": 1.0,
},
}
headers = {"xi-api-key": ELEVEN_KEY, "Content-Type": "application/json"}
with httpx.stream(
"POST", url, json=payload, headers=headers,
params={"output_format": "mp3_44100_128"}, timeout=30.0,
) as response:
response.raise_for_status()
with open(out_path, "wb") as f:
for chunk in response.iter_bytes():
f.write(chunk)
synthesize_eleven("Thanks for holding.", "21m00Tcm4TlvDq8ikWAM", "eleven.mp3")
Why this works: hitting the /stream variant instead of the plain endpoint lets playback begin before synthesis finishes. Meanwhile, stability and similarity_boost are the two knobs that actually change output character; lower stability produces more expressive but less predictable delivery, which is why long-form narration usually wants it higher.
Model choice matters a lot here. According to the ElevenLabs models documentation, Flash v2.5 targets roughly 75ms of model latency across 32 languages with a 40,000 character limit, whereas Eleven v3 covers 70+ languages but caps requests at 5,000 characters. In other words, you pick the model per workload rather than picking one for the whole app.
ElevenLabs also offers regional residency endpoints for the US, EU, India, and Singapore. If you have a data residency requirement in a contract, that alone can end the evaluation.
Cartesia Sonic: Streaming Built for Conversational Latency
Cartesia’s Sonic models target the real-time case directly. The HTTP endpoint is POST /tts/bytes, but the interesting one is the WebSocket at wss://api.cartesia.ai/tts/websocket, which supports continuations: you open a context, push transcript chunks as your LLM produces them, and Sonic keeps prosody coherent across chunks rather than restarting cold on every sentence.
# pip install websockets
import asyncio, base64, json, os, uuid
import websockets
API_KEY = os.environ["CARTESIA_API_KEY"]
WS_URL = "wss://api.cartesia.ai/tts/websocket?cartesia_version=2026-03-01"
async def stream_sonic(chunks: list[str], voice_id: str) -> bytes:
"""Feed LLM output into one Sonic context and collect raw PCM back."""
context_id = str(uuid.uuid4())
audio = bytearray()
async with websockets.connect(
WS_URL, additional_headers={"X-API-Key": API_KEY}
) as ws:
for i, chunk in enumerate(chunks):
await ws.send(json.dumps({
"context_id": context_id,
"model_id": "sonic-3.5",
"transcript": chunk,
"voice": {"mode": "id", "id": voice_id},
"output_format": {
"container": "raw",
"encoding": "pcm_s16le",
"sample_rate": 24000,
},
"language": "en",
# Keep the context open until the final chunk so prosody
# carries across sentence boundaries.
"continue": i < len(chunks) - 1,
}))
async for message in ws:
event = json.loads(message)
if event.get("type") == "chunk":
# Audio frames arrive base64-encoded inside the JSON envelope.
audio.extend(base64.b64decode(event["data"]))
elif event.get("type") in ("done", "error"):
break
return bytes(audio)
VOICE_ID = os.environ["CARTESIA_VOICE_ID"] # from your Cartesia voice library
asyncio.run(stream_sonic(["Hi there.", " How can I help today?"], VOICE_ID))
Why this works: the context_id plus continue pair is the whole point of the Cartesia design. Without it, each sentence is synthesized independently and you hear the seams, because the model has no idea it is mid-utterance. With it, you can start speaking the first clause while the LLM is still generating the rest of the response. That pattern pairs naturally with server-sent events versus WebSockets for streaming LLM responses, since you are effectively chaining two streams.
Sonic supports pcm_mulaw and pcm_alaw at 8000 Hz, which is exactly what telephony wants. Consequently, wiring Sonic to a phone number involves no resampling step. Cartesia lists sonic-3.5 as the default model, with sonic-3, sonic-preview, and sonic-latest also available, plus 40+ languages.
The trade-off is ecosystem maturity. Cartesia’s docs are thinner than the other two, portions require login, and the SDK surface is smaller. Therefore, budget time for reading the raw API reference rather than copying a quickstart.
How Much Does Each Text-to-Speech API Cost?
Pricing models differ enough that a straight comparison needs care. Here is what each vendor publishes.
| Vendor | Model | Published price |
|---|---|---|
| OpenAI | tts-1 | $15.00 / 1M characters |
| OpenAI | tts-1-hd | $30.00 / 1M characters |
| OpenAI | gpt-4o-mini-tts | $12.00 / 1M audio output tokens + $0.60 / 1M text input tokens |
| ElevenLabs | v2 / v3 | $0.10 / 1,000 characters on all paid tiers |
| ElevenLabs | Flash / Turbo | $0.05 / 1,000 characters |
| Cartesia | Sonic (Startup plan) | $49/month for 1.25M credits |
Normalizing those: tts-1 works out to $0.015 per 1,000 characters. ElevenLabs Flash at $0.05 is therefore roughly 3x that, and ElevenLabs v3 at $0.10 is closer to 7x. Cartesia’s Startup plan divides out to about $0.039 per 1,000 credits, though that figure is derived from plan price divided by included credits rather than a published unit rate, so treat it as an estimate and confirm against your own invoice.
One important caveat: gpt-4o-mini-tts bills audio output in tokens, not characters. Because token count scales with the duration of the generated speech rather than the length of the input text, verbose delivery costs more than terse delivery for identical input. As a result, you cannot forecast its cost from character counts alone. Measure a representative sample before you model spend, using the same discipline described in token counting and budget management for LLM apps.
For any workload with repeated phrases, cache aggressively. Greetings, hold messages, IVR prompts, and error strings are usually a large share of total synthesis volume, and they never change. A simple content-hash cache in object storage typically removes a substantial fraction of spend before you optimize anything else. The same reasoning behind semantic caching for LLMs applies here, except TTS caching is easier because exact-match hashing is sufficient.
How to Benchmark Time to First Byte Yourself
Published latency numbers exclude network time and say nothing about your region, your payload sizes, or your concurrency. Consequently, you should measure. This harness records time-to-first-audio-byte, which is the number that actually determines whether a conversation feels natural.
import time
import httpx
def time_to_first_byte(method: str, url: str, **kwargs) -> float:
"""Return seconds until the first audio byte arrives, not total duration."""
start = time.perf_counter()
with httpx.stream(method, url, timeout=30.0, **kwargs) as response:
response.raise_for_status()
for _ in response.iter_bytes():
return time.perf_counter() - start # first chunk only
raise RuntimeError("no audio returned")
def benchmark(label: str, runs: int, **kwargs) -> None:
samples = sorted(time_to_first_byte(**kwargs) for _ in range(runs))
p50 = samples[len(samples) // 2]
p95 = samples[int(len(samples) * 0.95) - 1]
print(f"{label}: p50={p50*1000:.0f}ms p95={p95*1000:.0f}ms")
Why this works: returning inside the iter_bytes() loop stops the clock at the first chunk instead of at completion, which is the metric that maps to perceived responsiveness. Reporting p95 alongside p50 matters because tail latency is what users complain about, and averages hide it completely.
Run at least 30 iterations per vendor, from the region where your servers actually live, using text lengths representative of your real traffic. Short prompts and 2,000-character paragraphs produce very different curves. Additionally, run some of it under concurrency, since rate limits and queueing behavior only surface under load. Pair that with sane rate limiting and retry strategies, because every one of these APIs will throttle you eventually.
Real-World Scenario: A Support Voice Agent’s Latency Budget
Consider a small team building a phone support agent for an e-commerce company, running over several weeks with a target of sub-second perceived response time. The pipeline is speech-to-text, then an LLM, then text-to-speech, then telephony. Each stage eats into one budget.
The common failure pattern is straightforward. The team measures each component in isolation, finds every one acceptable, and then discovers that end-to-end latency is roughly the sum of all of them plus network hops. Speech recognition might take a few hundred milliseconds after the caller stops talking, the LLM adds its own time-to-first-token, and TTS adds more on top. By the time audio reaches the caller, the pause reads as a dropped call, and callers start repeating themselves.
Two fixes usually matter more than swapping vendors. First, stream the LLM output into a persistent TTS context instead of waiting for the full response; that alone removes the LLM’s full generation time from the critical path, since synthesis of the first clause overlaps with generation of the rest. Second, output µ-law 8kHz directly rather than generating 44.1kHz MP3 and resampling, which removes both an encode and a decode step. Only after both are in place does the vendor’s model latency become the dominant term worth optimizing.
The trade-off is real, though. A streaming architecture with persistent contexts is meaningfully harder to debug than a request-response one, because failures are partial: half an utterance plays and then the context dies mid-sentence. Teams that adopt it need reconnection logic and a fallback to non-streaming synthesis before they ship. For a related look at end-to-end voice pipelines, see building voice agents with the Gemini Live API in Python.
When to Use Each Text-to-Speech API
Choose OpenAI TTS
- Your stack already authenticates against OpenAI, and one fewer vendor contract has real value
- You want prosody control through plain-English
instructionsrather than numeric tuning - The workload is batch or near-real-time: article narration, notifications, accessibility readouts
- Stock voices are acceptable and no cloning is required
Choose ElevenLabs
- The voice is a branded asset and must sound like a specific person
- You need emotional range for audiobooks, characters, or long-form narration
- Data residency in the US, EU, India, or Singapore is a contractual requirement
- You need broad language coverage, particularly the 70+ languages in Eleven v3
Choose Cartesia
- You are building a real-time conversational or telephony agent
- Prosody must stay coherent while text streams in from an LLM token by token
- You need µ-law 8kHz output without an intermediate resampling stage
- Per-character cost matters at high volume and stock voices are acceptable
When NOT to Use These Text-to-Speech APIs
Skip hosted TTS entirely when
- You are synthesizing a fixed, small set of strings; render once, cache the files, and stop paying per request
- Audio must never leave your infrastructure, in which case a self-hosted model is the honest answer
- Your budget cannot absorb per-character billing that scales linearly with usage forever
Avoid these specific vendors when
- OpenAI, if you need voice cloning, WebSocket streaming, or native telephony encodings
- ElevenLabs, if your volume is high and stock voices would have been fine, since you are paying a large premium for fidelity you do not use
- Cartesia, if you need mature SDKs, extensive community examples, and heavily documented edge cases today
Common Mistakes with Text-to-Speech APIs
- Benchmarking total request duration instead of time to first audio byte, which measures the wrong thing entirely for interactive systems
- Generating high-bitrate MP3 for a telephony pipeline that immediately downsamples it to 8kHz, wasting both latency and quality
- Skipping a cache for static phrases, then wondering why the bill scales with traffic rather than with content
- Treating vendor-published latency as end-to-end latency, when it excludes network and application time by definition
- Synthesizing each sentence as an independent request in a conversation, producing audible seams because the model restarts prosody every time
- Hardcoding one model per vendor instead of selecting per workload, for example using a flagship quality model for hold messages nobody listens to closely
- Ignoring character limits, which differ sharply by model and will reject long inputs at runtime rather than at deploy time
- Failing to handle mid-stream disconnects, leaving callers with half-finished sentences and no recovery path
Which Text-to-Speech API Should You Pick?
For most teams, the OpenAI TTS vs ElevenLabs vs Cartesia decision resolves cleanly once you name your hard constraint. Pick Cartesia for real-time voice agents and telephony, where streaming contexts and µ-law output remove entire stages from your critical path. Pick ElevenLabs when the voice is branded, cloned, or needs emotional range, and accept the price premium as the cost of that fidelity. Pick OpenAI when you want one less vendor and your latency budget is measured in seconds rather than milliseconds.
Whichever you choose, do the measurement before the migration. Run the time-to-first-byte harness above against two candidates from your production region for a week, with your real text lengths and real concurrency, and let the p95 numbers decide. Next, pair your synthesis layer with the transcription side by reading the guide to the OpenAI Whisper API for speech-to-text, which covers the other half of any voice pipeline.