TLDR: Pre-generate your TTS audio when you store the text — not when the caller requests it. A fan-out burst will expose your rate limits immediately, and a live phone call has zero tolerance for that.

the setup

I built my weekly review skill — it sweeps every business I'm involved in, builds a briefing, and at Step 5 calls my phone to read it to me.

The briefing runs long — thousands of words — so I chunk it at sentence boundaries into 21 pieces, store them in Upstash Redis (managed Redis) under a random UUID, and hand Telnyx (my telephony platform) a TeXML (Twilio-compatible call markup) document with 21 sequential <Play> tags, each pointing to /api/audio?id=X&chunk=k.

Clean architecture. Tested great.

what broke

First live call: "We're sorry, an application error has occurred."

Not what you want to hear when your phone rings.

Vercel logs confirmed it — Telnyx hit /api/twiml, then hit /api/audio 21 times, 21 error-level entries.

So I did the obvious thing: tested every chunk manually, sequentially. One curl per chunk. 21 curls. 21 × 200 OK.

Everything passed.

the trap

Here's the part I missed.

Telnyx doesn't request chunks sequentially. It sees 21 <Play> tags and goes. Fast. That's the actual access pattern — a burst, not a queue.

So I fired 21 concurrent curls at /api/audio.

Result: 15 × 502, 6 × 200.

There it was. ElevenLabs (my TTS provider) has a concurrency cap. And /api/audio had been synthesizing audio live on every request — fine when chunks come in one at a time, catastrophic when 21 land simultaneously.

The sequential test didn't lie. It just answered a question that nobody was actually asking.

the fix that worked

The fix is almost obvious once you see it.

Pre-generate every chunk at store time, not at serve time.

/api/briefing now synthesizes all 21 chunks sequentially during the store call — a simple loop, one chunk at a time, zero concurrency. Each mp3 gets cached as base64 in Redis under audio:{id}:{k}.

When the call connects, /api/audio just reads from cache. Zero live TTS on the hot path. (There's a fallback to a single live synth if a chunk somehow missed pre-gen — but that's a safety net, not the happy path.)

Commit: fix(audio): pre-generate + cache chunk TTS at store time (kill the burst 502s).

Briefings have run clean ever since.

why this matters to me

The ElevenLabs rate limit isn't really the lesson here.

The lesson is about the hot path.

When Telnyx connects a call, it starts fetching immediately. That's not the moment to kick off expensive, rate-limited, failure-prone work. There's no slack. There's no retry window. There's a live call and a very unforgiving concurrency ceiling.

Store time is when you have slack. Do the expensive work then.

This generalizes way past TTS — image processing, LLM completions, third-party API calls. If you can precompute it and cache it, do it when you're writing, not when you're reading. The hot path should be boring. Cache lookups are boring. That's the point.

P.S. The tell that my test was lying to me: it felt too easy. If your local probe passes and the live system fails, the gap between your test and the real access pattern is the bug. Fire the real pattern first.