DEV Community

Gerald T Chenwi
Gerald T Chenwi

Posted on Originally published at katush.online AI-assisted

We Replaced LiveKit with Cloudflare Workers for AI Voice Tutoring — and Kept the Same Brain

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)
Enter fullscreen mode Exit fullscreen mode

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

What broke in production (and what we'd tell you to fix on day one)

  1. Silent audio

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.

  1. Eight-second pauses

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.

  1. Orphan sessions (the nasty one)

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.

  1. Quota errors only in the console

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
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.

Top comments (1)

Collapse
 
spaiboss profile image
Gerald T Chenwi

Have you shipped Cloudflare @cloudflare/voice in production? What broke?