We Replaced LiveKit with Cloudflare Workers for AI Voice Tutoring — and Kept the Same Brain Katush, an AI tutor for Cameroon secondary students, replaced LiveKit with Cloudflare Workers and Durable Objects for its voice tutoring transport, keeping the same backend and BYOK stack (Deepgram, Gemini Flash Lite, Cartesia Sonic-2). The switch cut transport costs, not AI costs, with variable COGS still around $0.019/min. The team documented production issues such as PCM audio format mismatches and billing state races, and shared fixes including streaming TTS per sentence and releasing stale call rows. We run Katush, an AI tutor for Cameroon secondary students GCE prep . Voice tutoring was production-ready on LiveKit Cloud + a Python agent, but platform cost stacked on top of already-expensive STT/LLM/TTS APIs. We built a parallel path on Cloudflare Workers + Durable Objects + @cloudflare/voice, switched with one build flag, and kept the same BYOK stack: Deepgram, Gemini Flash Lite, Cartesia Sonic-2. Result: Cloudflare voice is live in production. We did not cut AI costs in half — we cut transport tax. Variable COGS is still ~$0.019/min. At 12 XAF/min retail, margin is thin unless the tutor stays brief. If you already pay for speech APIs and need a cheaper browser transport, this pattern works. If you need WebRTC on bad mobile networks or telephony, keep LiveKit. The problem nobody talks about in voice AI demos Voice agents look simple in a README: Mic → STT → LLM → TTS → Speaker In production there are two bills: Cognition — Deepgram, OpenAI/Google, Cartesia, etc. Transport — who hosts the agent loop and moves audio LiveKit, Cloudflare, Daily, … For Katush, cognition already eats most of our retail minute price. We sell voice at 12 XAF/min ~$0.02 USD . Students buy minute packs via mobile money Campay . They expect a wallet: "I have 7 minutes left" — not abstract token limits. LiveKit is excellent. WebRTC, turn detection, agent framework, recording roadmap. But LiveKit Cloud agent hosting sits on top of the API vendors. In a low-ARPU market, that extra platform layer hurts. So we asked: Can Cloudflare Workers + Durable Objects replace LiveKit transport while reusing our backend tutor logic and BYOK providers? Architecture: parallel stacks, one brain We did not rewrite the tutor. Both paths call the same Render backend: /api/agent/context — student profile, exam level, subject track /api/agent/rag/search — past papers and curriculum grounding Billing via Call + UsageLog VOICE CALL The only swap is how audio reaches the agent. LiveKit before Cloudflare after Session start POST /api/livekit/room POST /api/voice/cf/session/start Media WebRTC to LiveKit SFU WebSocket to Worker DO Agent Python on LiveKit Cloud TypeScript in Durable Object Session end DELETE /api/livekit/room POST /api/voice/cf/session/end Crash cleanup room finished webhook Orphan release + force-close Frontend switch — one env var at build time: typescript VITE VOICE PROVIDER=cloudflare // or livekit Same React modal. Same minute wallet. Same upgrade toasts. Worker URL production : https://katush-voice-cloudflare.spaitraceonline.workers.dev https://katush-voice-cloudflare.spaitraceonline.workers.dev What broke in production and what we'd tell you to fix on day one Symptom: Transcript updated. No sound. Cause: Gemini TTS returned raw PCM; the browser expected WAV/MP3. Fix: WAV header wrap; prefer Cartesia MP3 in production. Symptom: Text fast, speech slow. Cause: TTS waited for full LLM completion. Fix: Stream Gemini by sentence; invoke Cartesia per sentence. First audio dropped from ~8–12s to ~1s on many turns. Symptom: WebSocket closed before connection established POST /session/start → 403 active voice call in progress Cause: Backend creates a Call row at session/start, before the WebSocket succeeds. A failed connect left a stale row for up to 2 hours. Fixes: session/end on every client-side connect failure Release abandoned cf calls after 45s Force-close stuck rows on retry WebSocket connect retry 2 attempts, 45s timeout Don't bill failed connects under 15 seconds Lesson: Billing state must follow media state. LiveKit taught us this with webhooks; Cloudflare made us port the logic explicitly. Students hit "no minutes left" and saw a generic failure. We moved 403 reasons into toasts with upgrade links. In consumer EdTech, console.error is a silent bug. Economics honest numbers Assumptions: ~240 TTS characters/minute of tutor speech, 600 XAF/USD. Layer ~USD/min Deepgram Nova-3 $0.0048 Gemini 2.5 Flash Lite ~$0.001 Cartesia Sonic-2 ~$0.012 Cloudflare DO + Worker ~$0.0015 Total variable ~$0.019 ~11–12 XAF/min Fixed: Cloudflare Workers Paid ~$5/month. Render backend free tier became our first concurrency bottleneck ~10–25 concurrent session starts — not Cloudflare. Retail: 12 XAF/min ≈ break-even on variable COGS. TTS verbosity is a margin lever. We cap tutor replies in voice prompts to one to two short sentences. Switching transport did not remove API costs. It changed who hosts the agent loop ~$5/mo vs LiveKit agent hosting . LiveKit vs Cloudflare — quick decision guide Choose Cloudflare if: Browser-only tutoring TypeScript team Minimize platform fixed cost You can own WebSocket reliability + session billing Choose LiveKit if: Mobile WebRTC on lossy networks is core You need recording, SIP, or mature turn detection Rich LLM function tools out of the box We run Cloudflare primary, LiveKit one flag away for rollback. What we'd do differently Orphan release before the first production student Structured sessionId tracing across worker, backend, browser Mobile dropout study MTN/Orange before declaring victory Consider 14–16 XAF/min for purchased packs if the tutor runs chatty Try it / build your own Health check: GET https://katush-voice-cloudflare.spaitraceonline.workers.dev/health https://katush-voice-cloudflare.spaitraceonline.workers.dev/health Product: katush.online If you're building voice in a price-sensitive market: swap transport, not brain. The hard part is billing hygiene and TTS economics — not finding another LLM.