Skip to content
Get started
Get started

SDK Performance: TypeScript, Python, PHP, Go, Rust & Elixir

This report compares all six official network SDKs against the same bunqueue broker. It is designed to answer two different questions:

  1. How much producer throughput can each client sustain before the broker and SQLite become the limiting system?
  2. How efficiently does each Worker refill its concurrency slots when handlers are short and I/O-bound?

The results were measured on 20 July 2026 at repository revision 8f276e03ae480bc1abc615869769e48e087f7804 (bunqueue 2.8.43). They are a dated engineering result, not a universal capacity promise.

ComponentVersion
HostApple M1 Max, 10 cores, 32 GiB, arm64
Operating systemmacOS 26.5.2 (25F84)
Broker runtimeBun 1.3.14
PythonCPython 3.14.3
PHPPHP CLI 8.5.8, NTS
GoGo 1.26.5
Rustrustc/cargo 1.97.0, release build
ElixirElixir 1.20.2, Erlang/OTP 29

Every measured sample used a fresh native Bun broker, SQLite database, dynamic TCP and HTTP ports, and unique queue. No benchmark number came from Docker or a VM. SDK order rotated between samples to reduce thermal and ordering bias.

Each SDK/scenario combination received one unmeasured warm-up followed by five independent measured samples. External sampling recorded the client and broker process-tree RSS every 50 ms. Setup, preload, shutdown, and final verification were excluded from timed regions.

Across measured samples the campaign submitted 600,000 producer jobs and processed 150,000 worker jobs. Every sample ended with the exact expected server-side job count. This is a conservation check, not a claim of exactly-once handler execution; bunqueue’s delivery contract remains at-least-once.

Each sample submits 20,000 jobs with an approximately 256-byte payload through:

  • eight concurrent connections/producers;
  • addBulk batches of 50 jobs;
  • 400 measured bulk calls per sample;
  • local TCP and MessagePack, using each SDK’s real production client.

Throughput is total accepted jobs divided by elapsed wall-clock time. Call latency percentiles are calculated inside each sample; the table reports the median p50, p95, and p99 across its five samples. CV is the coefficient of variation of sample throughput: standard deviation divided by mean.

SDKMedian jobs/sMean jobs/sMin–max jobs/sCVp50p95p99Peak client RSS
Rust release63,73562,98561,347–63,8591.63%3.936 ms15.734 ms18.800 ms8.0 MiB
PHP62,05361,73760,464–62,6751.22%4.365 ms15.274 ms19.443 ms73.2 MiB
Go61,66561,47260,664–61,9660.73%4.243 ms15.698 ms19.292 ms17.3 MiB
TypeScript/Bun61,51161,42660,254–62,5701.39%4.404 ms17.048 ms20.271 ms79.4 MiB
Python60,70160,99360,325–61,9601.07%4.252 ms16.508 ms19.604 ms29.2 MiB
Elixir/OTP60,42960,17658,046–61,1781.86%4.423 ms16.829 ms23.829 ms109.7 MiB

PHP RSS is the aggregate of eight producer processes, while the other rows are single-runtime process trees. RSS also includes each runtime’s allocator, garbage collector, loaded standard library, and JIT where applicable; it is an operational footprint measurement, not bytes retained per job.

The fastest and slowest medians differ by only 5.5%. All six SDKs therefore reach the same broad broker-bound plateau. Rust leads by roughly 3.6% over TypeScript and has the lowest client RSS and p99, but changing producer language alone will not multiply queue capacity in this workload.

The tail is also compact: five SDKs remain below 20.3 ms p99, while Elixir reaches 23.8 ms. Go has the lowest throughput variation. Every producer CV is below 2%, so the ranking is stable on this host, although the small gaps should not be treated as architectural differences.

For production producers, batching is the important decision. A sequential add() pays one command round trip per job; this workload amortizes framing, MessagePack decoding, dispatch, and persistence over 50 jobs while eight connections keep the broker supplied.

Each sample starts with 5,000 preloaded jobs and drains them with:

  • a 1 ms simulated I/O handler;
  • total concurrency of 16;
  • batch pulls over real TCP connections;
  • an individual ACK for every completed job;
  • final polling until the server reports exactly 5,000 completed jobs.

TypeScript, Python, Go, Rust, and Elixir use one SDK Worker configured for 16-way concurrency. PHP’s Worker is sequential by design, so its idiomatic equivalent is 16 independent CLI worker processes. The timer measures the complete drain, including pull, handler, ACK, and worker refill time.

ACK batching was disabled for comparability. TypeScript and Python support opt-in ACKB; enabling it could reduce acknowledgement round trips, but would give those two clients a protocol optimization unavailable in the other rows.

SDKMedian jobs/sMean jobs/sMin–max jobs/sCVPeak client RSS
Rust release2,178.22,166.62,111.1–2,212.41.88%8.8 MiB
Elixir/OTP2,136.92,135.82,108.9–2,157.50.74%109.8 MiB
PHP, 16 processes1,297.91,301.51,283.2–1,321.20.98%132.5 MiB
Go662.2664.4661.0–669.20.50%17.9 MiB
TypeScript/Bun657.9657.8656.8–658.20.07%79.9 MiB
Python268.0267.7266.4–268.10.24%29.3 MiB

PHP memory is the aggregate RSS of all 16 worker processes. Its throughput and memory must therefore be read as a deployment topology, not as one PHP Worker.

This workload intentionally makes handlers shorter than the SDK polling interval. It exposes the control loop rather than application compute.

The TypeScript Worker calculates free = concurrency - active.size. When no slot is free it sleeps for 20 ms before checking again. Go uses the same 20 ms fixed wait. Python waits 50 ms. Completed handlers cannot wake these sleeps, so new jobs are not pulled immediately when slots become free.

With 16 slots and a 1 ms handler, the approximate refill ceilings are:

SDK loopFixed full-capacity waitApproximate ceilingObserved
TypeScript20 ms800 jobs/s658 jobs/s
Go20 ms800 jobs/s662 jobs/s
Python50 ms320 jobs/s268 jobs/s

The remaining gap includes pull/ACK round trips, scheduling, handler execution, and imperfect alignment between slot completion and the polling timer.

Rust pulls a bounded batch, joins its processing threads, and immediately begins the next iteration. Elixir similarly waits for its Task.async_stream batch and recurses directly into the next pull. Neither inserts a fixed success-path sleep between full batches. This explains their approximately 2.1K jobs/s without implying that their MessagePack codecs are three to eight times faster.

PHP reaches roughly 1.3K jobs/s by scaling across 16 independent connections. That topology refills work independently but spends more aggregate memory and operating-system process resources.

The first SDK performance change should be event-driven slot refill in TypeScript, Go, and Python:

  1. signal a condition, channel, semaphore, or promise when an active handler releases its slot;
  2. let a full Worker wait on that signal instead of a fixed timer;
  3. pull again immediately when capacity becomes available;
  4. keep bounded backoff only for empty queues and transport errors;
  5. evaluate ACK batching after removing the refill ceiling.

This changes the worker loop from timer-quantized scheduling to capacity-triggered scheduling. It should matter most for short database, cache, HTTP, and event-routing handlers. Long-running or CPU-bound handlers will see less benefit because processing time already dominates the 20–50 ms wait.

Use the producer table to size ingestion-heavy services that send moderate payloads in bulk over a low-latency network. Use the worker table to compare SDK control-loop overhead for very short I/O handlers. Neither table predicts:

  • WAN performance, where network RTT can dominate;
  • large payloads, where serialization and copies become material;
  • durable-per-job writes, which change the persistence cost;
  • CPU-bound handlers, which need process/thread isolation;
  • queues dominated by delays, priorities, groups, dependencies, or retries;
  • horizontal broker scaling, because every sample uses one local broker.

Do not extrapolate linearly from five-second-scale drains to 24/7 capacity. Production sizing also needs a sustained soak with the application’s real payload distribution, handler latency, retry rate, durability mode, disk, and network.

Before benchmarking, the complete isolated SDK gate passed sequentially:

SDKNative testsProtocol conformance
TypeScript15917/17
Python13717/17
PHP6517/17
Go72, 1 skipped17/17
Rust45, 1 skipped17/17
Elixir49, 1 skipped17/17

The gate verifies package builds, native behavior, real broker integration, TLS, reconnect, leases, retries, flows, telemetry, and the shared protocol contract. It is separate from the native performance campaign: containers provide clean functional isolation, while only host-native runs are accepted as benchmark evidence.

See all bunqueue benchmarks, the SDK API guide, and the client SDK architecture for the surrounding transport and worker design.