Performance
pipedog on Bun, Node, Deno and Cloudflare Workers against Pipecat on CPython, measured on the same machine with the same scenarios.
pipedog and Pipecat run the same pipelines: a frame goes through the same processors, the same
queues and the same PipelineWorker. This page measures what that costs on each runtime, offline
(the framework alone) and against Gemini (a real service, where the network dominates).
Every scenario is written twice, once against each framework, in the same shape:
tools/bench/perf/pipedog.ts
and
tools/bench/perf/pipecat_bench.py.
Pipecat runs from the upstream commit pipedog is ported from. Both log at WARNING. pipedog runs
from a bundle made with bun build, as it is deployed. The runtimes take turns within each round,
so a slow minute on the network or on the machine falls on all of them.
Results
Frame processing
10 pass-through processors in a PipelineWorker. Throughput: 20,000 TextFrames queued at once.
Latency: one frame at a time, queued to arrival at the end of the pipeline. In brackets: compared with Pipecat (higher is better).
| Throughput (frames/s) | Latency per frame (median) | p90 | Peak memory | |
|---|---|---|---|---|
| Pipecat · CPython 3.13.13 | 9,294 (1.0×) | 572 µs (1.0×) | 578 µs | 144 MB |
| pipedog · Bun 1.4.2 | 44,394 (4.8×) | 52 µs (11.0×) | 60 µs | 184 MB |
| pipedog · Node 24.14.0 | 52,350 (5.6×) | 41 µs (13.8×) | 58 µs | 278 MB |
| pipedog · Deno 2.9.5 | 53,288 (5.7×) | 59 µs (9.7×) | 74 µs | 221 MB |
| pipedog · workerd 1.20261005.1 | 45,198 (4.9×) | 180 µs (3.2×) | 239 µs | – |
Cold start
From starting the process to a pipeline that has started and ended, with the Google services loaded
(for workerd: from starting workerd serve to the response). Runtime alone is the same for an
empty program, so the difference is what loading the framework and running the pipeline cost.
| Cold start | Runtime alone | Framework | Peak memory | Bundle | |
|---|---|---|---|---|---|
| Pipecat · CPython | 396 ms | 13 ms | 383 ms | 114 MB | – |
| pipedog · Bun | 44 ms | 6 ms | 38 ms | 51 MB | 3.5 MB |
| pipedog · Node | 66 ms | 21 ms | 45 ms | 87 MB | 3.8 MB |
| pipedog · Deno | 47 ms | 20 ms | 27 ms | 90 MB | 3.1 MB |
| pipedog · workerd | 42 ms | 11 ms | 31 ms | – | 1.7 MB |
Gemini LLM: time to first token
GoogleLLMService with gemini-3.5-flash-lite (thinking at its minimum, as the service sends by default), from queueing an LLMContextFrame to the first LLMTextFrame at the end of the pipeline. SDK alone is the same request made with the Google GenAI SDK of the same language, in the same process, alternating with the pipeline.
| Through the pipeline (median) | SDK alone (median) | Difference, paired (median) | Middle half of differences | Requests | |
|---|---|---|---|---|---|
| Pipecat · CPython | 529 ms | 491 ms | -6 ms | -41 to 104 ms | 36 |
| pipedog · Bun | 497 ms | 452 ms | 15 ms | -24 to 116 ms | 36 |
| pipedog · Node | 516 ms | 500 ms | -21 ms | -173 to 166 ms | 36 |
| pipedog · Deno | 527 ms | 513 ms | 40 ms | -30 to 93 ms | 36 |
| pipedog · workerd | 496 ms | 467 ms | 51 ms | -68 to 129 ms | 36 |
Gemini TTS: time to first audio
GeminiTTSService with gemini-3.8-flash-tts, from queueing a TTSSpeakFrame to the first TTSAudioRawFrame at the end of the pipeline, and the same request made with the SDK.
| Through the pipeline (median) | SDK alone (median) | Difference, paired (median) | Middle half of differences | Requests | |
|---|---|---|---|---|---|
| Pipecat · CPython | 758 ms | 664 ms | 127 ms | 15 to 204 ms | 24 |
| pipedog · Bun | 741 ms | 626 ms | 127 ms | 43 to 192 ms | 24 |
| pipedog · Node | 753 ms | 727 ms | 30 ms | -42 to 117 ms | 24 |
| pipedog · Deno | 786 ms | 803 ms | 84 ms | -126 to 253 ms | 24 |
| pipedog · workerd | 876 ms | 642 ms | 170 ms | 92 to 294 ms | 24 |
Measured 2026-10-22 on Apple M4 Pro (12 cores, 24 GB), darwin 25.6.0: pipedog 0.8.0, Pipecat 5558d26dca (pinned upstream) on CPython 3.13.13, Bun 1.4.2, Node 24.14.0, Deno 2.9.5, workerd 1.20261005.1. 6 rounds, runtimes taking turns.
Reading the numbers
Frame processing is the framework alone: no I/O, no models. It's what a pipeline spends moving frames between processors, so it sets the floor under every response. In a voice bot each audio chunk (every 10–20 ms) and each LLM token goes through it.
Workers run pipedog's event loop the slowest of the JavaScript runtimes. Its timers and
setImmediate wait about a millisecond, so @pipedog/asyncio yields through a body read instead,
and a frame takes several such yields across 10 processors. That is still well under a
millisecond per frame.
Peak memory in the JavaScript runtimes is mostly garbage the collector hasn't reclaimed yet,
not memory in use. With 20,000 frames sent through, the live heap after a forced collection grows
by about 5 MB on Node, and drops back once the pipeline ends. CPython frees objects as soon as the
last reference goes, so its peak stays closer to what it holds. Under memory pressure V8 collects
earlier; --max-old-space-size caps it.
Cold start matters for serverless (a Worker or a function that starts per call) and for scaling out. Pipecat's is mostly importing Python modules (here with the Google services). A bundle loads in one read.
Gemini times are dominated by the network and the model, which vary by hundreds of milliseconds from one request to the next. The SDK alone column makes the same request with Google's SDK, in the same process, right before or after each pipeline request. The paired difference is the framework's share, and is mostly within the noise for both frameworks. TTS services send audio downstream in chunks of a fixed size (as Pipecat does), so the first audio frame waits for the first full chunk, while the SDK column stops the clock at the first byte.
Telephony audio
On a phone call, the bot's speech goes through the same steps on every call: the text-to-speech audio (usually 24 kHz) is resampled to the provider's rate (8 kHz for Twilio, Telnyx and others), μ-law encoded, and sent over a WebSocket in small messages paced in real time. These steps decide what the caller hears, so they are measured directly against Pipecat's.
Resampling and μ-law
Pipecat resamples with libsoxr at its highest quality (VHQ). pipedog can't load native code on
every runtime, so it uses a windowed-sinc resampler compiled to WebAssembly
(@pipedog/resampler-wasm), with a pure-TypeScript fallback. Both get the same test signal: three
tones they must keep (300 Hz, 1 kHz, 2.5 kHz) and three above 4 kHz that a resampler going down to
8 kHz must remove. μ-law is checked bit for bit against Pipecat's (Python's audioop).
| Resampling | Pipecat (libsoxr, VHQ) | pipedog (WebAssembly) | pipedog (TypeScript fallback) | 20 ms chunks vs one go: Pipecat / pipedog |
|---|---|---|---|---|
| 24 kHz → 8 kHz | -83 / -104 dB | -88 / -98 dB | -88 / -95 dB | -80 dB / identical |
| 24 kHz → 16 kHz | -83 / -105 dB | -87 / -100 dB | -88 / -100 dB | -81 dB / identical |
| 16 kHz → 8 kHz | -83 / -102 dB | -87 / -100 dB | -87 / -97 dB | -80 dB / identical |
| 8 kHz → 16 kHz | -82 / -97 dB | -86 / -98 dB | -86 / -96 dB | -80 dB / identical |
| 24 kHz → 48 kHz | -82 / -100 dB | -85 / -99 dB | -86 / -101 dB | -81 dB / identical |
| 48 kHz → 16 kHz | -83 / -107 dB | -87 / -100 dB | -88 / -100 dB | -81 dB / identical |
Each cell: everything besides the tones (noise, aliasing, distortion) / the largest single artefact, relative to the tones. Lower is cleaner; both are far below what can be heard.
μ-law: all 65,536 16-bit values encoded: identical to Pipecat's; all 256 codes decoded: identical.
Pacing
A bot plays two 6-second utterances through the WebSocket server transport and the Telnyx serializer, and a separate process standing in for Telnyx records when each audio message arrives. The dropouts are what a playback buffer of that size would run out of: silences a caller would hear. Each case runs with the bot's event loop idle and kept busy by other work, standing in for VAD, other calls and LLM streaming. Both frameworks use the same pacing code; the message sizes differ because libsoxr's stream resampler returns audio in blocks, while pipedog's returns it sample by sample, so pipedog sends a small message about every 40 ms and Pipecat a larger one every 120–160 ms. Telephony providers buffer more than 40 ms, so neither drops audio in practice.
| Event loop busy | Gap between messages, median / worst | Dropouts, 20 ms buffer | Dropouts, 40 ms buffer | Dropouts, 80 ms buffer | |
|---|---|---|---|---|---|
| Pipecat · CPython | 0% | 121 / 163 ms | 8 (27 ms of silence) | none | none |
| pipedog · Bun | 0% | 40 / 48 ms | none | none | none |
| pipedog · Node | 0% | 40 / 44 ms | none | none | none |
| pipedog · Deno | 0% | 40 / 47 ms | none | none | none |
| Pipecat · CPython | 40% | 122 / 163 ms | 8 (28 ms of silence) | none | none |
| pipedog · Bun | 40% | 40 / 60 ms | none | none | none |
| pipedog · Node | 40% | 41 / 53 ms | none | none | none |
| pipedog · Deno | 40% | 39 / 50 ms | none | none | none |
| Pipecat · CPython | 70% | 125 / 167 ms | 7 (36 ms of silence) | none | none |
| pipedog · Bun | 70% | 41 / 43 ms | none | none | none |
| pipedog · Node | 70% | 41 / 43 ms | none | none | none |
| pipedog · Deno | 70% | 38 / 47 ms | none | none | none |
Running it
bun install
GOOGLE_API_KEY=... bun run bench:perf --write # without a key, the Gemini scenarios are skipped
bun run bench:perf --runtime bun,python --scenario hop --rounds 1
bun run bench:audio --write # resampling and μ-law against Pipecat's
bun run bench:pacing --write # telephony pacing (--runtime, --load to narrow it down)The first run creates a Python 3.13 environment with uv and installs
Pipecat from the pinned upstream checkout (bun run upstream restores it). --write saves the raw
samples to
tools/bench/perf/results.json
and the tables above. Model inference (Silero VAD, Smart Turn) has its own benchmark,
bun run bench; its results are in the @pipedog/onnx
README.
pipedog is an unofficial community TypeScript port of Pipecat. It is not affiliated with or endorsed by Daily or the Pipecat project.
Portions adapted from the Pipecat documentation, Copyright (c) 2024–2025 Daily, BSD-2-Clause (see apps/docs/LICENSE-pipecat-docs).