Programming

Rust vs JS/TS Binaries: Backend API Performance

Compare Rust performance vs JS/TS binaries (Bun, Deno) for backend API servers: throughput, latency, CPU/memory, concurrency stability. Benchmarks show Rust edges in predictability; JS/TS viable for fast development.

1 answer 1 view

How do binaries compiled from JavaScript/TypeScript compare with native Rust binaries in runtime performance for backend API servers?

I understand JS/TS executables are often much larger (~10×), but if deployment size is acceptable and development speed matters, what are the performance trade-offs?

Specifically:

  • How do JS/TS binaries compare with Rust in throughput, request latency, CPU and memory usage under load, and startup/cold-start time?
  • How predictable and stable is performance under high concurrency?
  • How much does framework choice affect performance? Example frameworks (approximate performance order):
  • JavaScript/TypeScript: Bun + Elysia, Node.js + Fastify, Node.js + Express, Node.js + Nest
  • Rust: Actix Web, Axum, Rocket
  • Do tools that produce JS/TS executables (e.g. Bun: Generate executables from TypeScript or JavaScript and Deno: turn JavaScript and TypeScript into binaries) meaningfully narrow the performance gap?
  • For a backend API where meeting tight deadlines is important, is choosing JS/TS compiled to a binary a reasonable trade-off versus Rust — and in which scenarios is Rust worth the extra development effort?

What concrete benchmarks, metrics, and tuning tips should I use to make an informed decision between JS/TS (compiled to executables) and Rust for production backend APIs?

Rust performance for backend API servers typically outpaces JavaScript/TypeScript executables in throughput, latency, CPU and memory efficiency, and yields much lower tail-latency jitter. Bundled JS/TS binaries (bun js, deno compile) remove packaging and cold-start pain and can get close on moderate workloads, but they usually use more memory/CPU and show higher p95/p99 variance under heavy concurrency. If deployment size is acceptable and development speed matters, compiling JS/TS to a binary is a solid trade-off for many APIs; if you need extreme throughput, tight p99 SLOs, or CPU-bound work, Rust is often worth the extra development effort.


Contents


Performance summary: Rust performance vs JS/TS binaries

Short version: Rust binaries generally deliver higher throughput, lower median latency and much lower tail latency (p95/p99) per instance than bundled JavaScript/TypeScript binaries; Bun and Deno compiled executables reduce startup cost and packaging friction but don’t eliminate GC/JIT overhead or single-thread limitations. Real-world numbers depend on the workload and framework, but independent community comparisons consistently show Rust ahead in raw throughput and predictability (see the DEV Community comparisons and TechEmpower results). For example, community benchmarks report Rust servers serving tens of thousands to >100k RPS with p50/p95 latencies in low milliseconds, while Bun/Deno/Node variants usually fall behind by 10–40% in throughput and show higher p95/p99 scatter under load (sources linked below) https://dev.to/hamzakhan/rust-vsnodejs-vs-go-performance-comparison-for-backend-development-2g69, https://www.techempower.com/benchmarks/.


Throughput, latency, CPU and memory — side‑by‑side metrics

What to expect (approximate, from multiple community benchmarks and framework tests):

  • Throughput (RPS, single-instance synthetic tests)

  • Rust (Actix/Axum): often tens of thousands to >100k RPS in tight benchmarks; examples: ~60k–110k RPS depending on payload and test setup.

  • Bun (Elysia) / Bun-built binaries: commonly in the 40k–80k RPS band in similar tests.

  • Node + Fastify: ~30k–45k RPS (varies). Node + Express/Nest: lower (~20k–35k).
    (Sources: DEV showdown, TechEmpower, community service benchmarks) https://dev.to/hamzakhan/rust-vs-go-vs-bun-vs-nodejs-the-ultimate-2024-performance-showdown-2jml

  • Latency (median/p95/p99)

  • Rust: p50 often ~1–2ms on minimal JSON endpoints; p95/p99 stay close to median (low jitter).

  • Bun/Deno binaries: p50 ~2–4ms; p95 modestly higher; p99 sometimes spikes under GC or saturation.

  • Node: p50/p95 higher and more variable (observed p50 3–6ms, p95 higher depending on concurrency).
    (See community comparisons and Mezmo’s real-world REST API example) https://www.mezmo.com/blog/coding-for-performance-why-we-chose-rust

  • CPU

  • Rust usually uses less CPU to serve the same RPS because compiled Rust produces tighter code and avoids garbage-collection cycles. Benchmarks report Rust cores at lower average utilization for the same throughput.

  • JS/TS runtimes (V8 or other engines) require more CPU per request on average; Bun/Deno reduce some overhead but not all.

  • Memory

  • Rust binaries frequently show smaller resident sets and shorter memory growth under load; minimal Rust services can be tiny.

  • JS/TS binaries are commonly ~2–4× larger in RAM for similar workloads (observed ranges: Rust tens of MBs vs JS/TS tens to low hundreds MBs depending on libraries and runtime). Mezmo recorded striking deltas in a REST API test (Rust ~72k RPS, Node ~8k RPS in their setup), but real numbers depend on app complexity https://www.mezmo.com/blog/coding-for-performance-why-we-chose-rust.

  • Startup / cold-start

  • Rust static binaries often start fastest (<50 ms in tight tests).

  • Bun/Deno compiled binaries typically start in the 100–250 ms range; much faster than unpacking Node modules, but slower than optimized Rust in many tests.

  • Node (unbundled) usually has the slowest cold-start due to JIT and module loading (~200–400+ ms in many setups). See Bun/Deno executable docs for how they address packaging https://bun.com/docs/bundler/executables, https://deno.com/blog/deno-compile-executable-programs.

Caveat: those are synthetic and microbenchmarks (plaintext/JSON echo, minimal logic). Real endpoints that call databases, networks, or do I/O shift the bottleneck away from raw language/runtime speed; in those cases the difference narrows.


Predictability and stability under high concurrency

Predictability (tail latencies, variance) is where Rust most clearly shines:

  • Why? Rust avoids stop-the-world garbage collection, and its zero-cost abstractions + native code produce consistent execution. Community tests show low latency variance for Rust even at high concurrency; e.g., latency variance <5% in some 1k-concurrency tests cited in community comparisons.
  • JS/TS runtimes use garbage collection and JIT – both can introduce pauses, warm-up variability, and higher p99s. Node in some tests had 10–15% jitter; Bun narrows jitter vs Node but not to Rust levels in most tests.
  • Threading model matters: Rust async runtimes (Tokio, Actix) can use multi-threaded executors that scale across cores by default. JavaScript event loops are single-threaded for JS execution; to utilize multiple cores you generally run multiple processes (cluster, PM2, containers) or use worker threads for CPU tasks. That architecture adds orchestration complexity and changes how you tune pools and concurrency.

Operational implication: if you care about predictable p99s or need to absorb sudden spikes with graceful latency behavior, Rust will give you an easier path to stable SLOs; JS/TS requires more careful GC and process management to hit similar tail-latency guarantees.


Framework impact: javascript backend stacks and rust performance

Framework choice moves the needle a lot — sometimes more than the language itself.

Approximate performance order seen in community tests (highest → lower):

  • JavaScript / TypeScript (higher to lower)

  • Bun + Elysia (fast, lightweight)

  • Node + Fastify

  • Node + Express

  • Node + Nest (heavier, more features)

  • Rust (higher to lower)

  • Actix Web (often top in many benches)

  • Axum (Tokio-based, very good)

  • Rocket (ergonomic, slightly behind in raw throughput)

Why the spread? Minimal, zero-allocation-friendly frameworks add less per-request overhead. Feature-rich frameworks (ORM-heavy, many middleware layers) add latency and allocations. TechEmpower shows Rust frameworks often dominating plaintext throughput benchmarks, while JS frameworks occupy mid-to-lower tiers (context: plaintext tests favor minimal frameworks and tuned stacks) https://www.techempower.com/benchmarks/. In practice:

  • If you need maximal raw speed, pick Actix or a minimal Rust hyper-based stack.
  • If you want JS/TS ergonomics and good speed, prefer Bun + Elysia or Node + Fastify.
  • If you use expressive frameworks (Nest, Rocket), expect slower raw numbers but possibly faster development due to abstractions.

Do Bun / Deno executables meaningfully narrow the gap?

Short answer: They help — mostly for packaging and cold-starts — but they don’t fully erase runtime characteristics.

  • Gains from compiling JS/TS to executables:

  • Single-file deployment, faster startup than raw Node module resolution, simplified ops (no Node install required). See Bun and Deno compile docs for details https://bun.com/docs/bundler/executables, https://deno.com/blog/deno-compile-executable-programs.

  • Some benchmarks show Bun binaries closing the throughput gap by ~10–30% compared to Node, and reducing cold-start time substantially vs Node.

  • Remaining gaps:

  • Garbage collection and runtime memory management remain; compiled binaries still run on a GC’d engine (so p99 effects persist).

  • Single-threaded JS event model still applies — multi-core scaling generally requires multiple processes.

  • For raw CPU-bound throughput, native Rust still outperforms due to zero-cost abstractions and native codegen.

Community benchmarks (DEV Community, Medium tests) consistently report Bun/Deno narrowing but not eliminating the advantage Rust enjoys in throughput and especially in predictable tail latency https://dev.to/hamzakhan/rust-vs-go-vs-bun-vs-nodejs-the-ultimate-2024-performance-showdown-2jml, https://medium.com/deno-the-complete-reference/url-shortener-service-benchmarking-bun-elysia-vs-rust-actix-edfdf1609d15.


Trade-offs when deadlines matter: typescript api vs Rust

If time-to-market and developer velocity dominate:

  • Advantages of compiled JS/TS (Bun/Deno or Node with bundling)

  • Speed of iteration and a huge ecosystem of libraries.

  • Lower ramp for teams already proficient in JS/TS — less hiring friction.

  • Packaging to an executable addresses deployment friction and cold-starts for many serverless or containerized flows.

  • Good choice for moderate traffic APIs, prototypes, or when the team needs to deliver features quickly.

  • When Rust is worth the extra effort

  • You need single-instance extreme throughput (tens of thousands to >100k RPS on one box).

  • Tight tail-latency SLOs (p99) under heavy load.

  • CPU-bound workloads (compression, crypto, transforms) where native performance matters.

  • Memory-constrained environments where lower RSS and predictable memory use matter.

  • Long-lived production systems where safety and correctness (and the guarantees Rust gives) reduce long-term ops/debugging cost.

Practical rule of thumb (approximate):

  • If target single-instance RPS is under ~30–40k and p95 SLO is not extremely tight (say p95 < 50–100ms is ok), a compiled JS/TS backend is reasonable.
  • If you target single-instance p95 in single-digit milliseconds at high RPS (>50k) or need very low p99s, consider Rust.

Those thresholds are approximate; you should validate them with your own workload.


Concrete benchmarks, metrics and tuning tips (how to test)

Run your own representative tests. Here’s a practical plan, commands, and what to measure.

  1. Test types (run each for realistic payloads)
  • Plaintext / simple JSON echo (baseline overhead)
  • Small JSON response (50–200 B)
  • Medium JSON response (500–2,000 B)
  • DB-backed endpoint (Postgres/MySQL) with realistic DB latency (tune pool)
  • CPU-bound endpoint (e.g., HMAC sign / hash)
  • Spike & ramp tests (to observe cold-start and saturation behavior)
  1. Tools (pick 2): wrk/wrk2, k6, Fortio, or vegeta. Example wrk2 fixed-rate command:
bash
# wrk2 (fixed rate) — requires wrk2/wrk with -R support
wrk -t12 -c400 -d60s -R50000 --latency http://127.0.0.1:3000/

Example k6 run for scripted scenarios:

bash
k6 run --vus 200 --duration 1m script.js
  1. Metrics to collect (essential)
  • Throughput (aggregate and per-second) — RPS achieved vs intended.
  • Latency distribution — p50, p90, p95, p99, max; latency histogram.
  • Error rate / HTTP errors.
  • CPU utilization per core and overall.
  • Memory RSS and heap-related stats (for JS: process.memoryUsage()).
  • GC pause durations and frequency (for V8/Deno/Bun where available).
  • File descriptors, connection saturation, socket queues.
  • System metrics: network, disk, interrupts, context-switches.
  1. Profiling and observation
  • Node: run with --trace-gc or --perf-basic-prof for GC and CPU insights; use clinic / 0x for flamegraphs.
  • Bun/Deno: consult runtime docs and measure with system profilers; they expose less mature tooling compared to Node but still can be profiled.
  • Rust: use cargo build --release and profile with perf, cargo flamegraph, or pprof crates for hotspots.
  • Collect system-level metrics with pidstat, htop, docker stats, or Prometheus + Grafana.
  1. Build and deployment tuning (examples)
  • Rust: compile optimized: RUSTFLAGS="-C target-cpu=native" cargo build --release. In Cargo.toml, for max performance set:
toml
[profile.release]
opt-level = 3
lto = true
codegen-units = 1
panic = "abort"
  • Rust runtime tuning: set worker threads to match cores for CPU-bound; for I/O-bound tasks you may set slightly fewer or tune runtime accordingly (Actix: .workers(num_cpus::get()); Tokio runtime builder: worker_threads = num_cpus).
  • Node: use Fastify instead of Express for performance-critical endpoints; set --max-old-space-size to control heap; log GC (--trace-gc) during tests to understand pauses. Use clustering or process manager (PM2) to use multiple cores.
  • Bun/Deno: use their compile-to-binary workflows to reduce cold-start; check docs https://bun.com/docs/bundler/executables, https://deno.com/blog/deno-compile-executable-programs.
  1. Example pass/fail decision checks after testing
  • Can one instance meet your RPS and p95/p99 SLOs while staying below acceptable CPU and memory limits? If yes, you can use that stack.
  • If not, measure hotspots, then either tune (pool sizes, thread counts, reduce allocations, switch framework) or choose a different runtime (Rust) or scale horizontally.
  1. Tuning checklist (quick)
  • Profile before changing code.
  • Reuse connections: HTTP keep-alive, DB pools.
  • Avoid sync/blocking calls on the main event loop.
  • Reduce allocations in hot paths (buffer reuse, streaming).
  • Offload TLS termination / heavy transforms if useful.
  • Use horizontal scaling and health checks — real traffic is bursty.

Running these realistic tests will show whether the convenience of TypeScript and a binary build meets your production SLOs — or whether Rust’s extra effort buys you necessary headroom.


Sources


Conclusion

Rust performance for backend APIs tends to be measurably better in throughput, latency, CPU and memory efficiency and far more predictable at scale; compiled JS/TS (bun js, deno compile) meaningfully improves packaging and startup and can be entirely adequate when developer speed and deadlines matter. Run representative benchmarks (RPS, p50/p95/p99, CPU/memory, GC behavior) for your workload: if a compiled JS/TS typescript api meets your SLOs with acceptable headroom, choose it for speed of delivery; if you need single-instance high throughput, very low tail latency, or efficient CPU-bound processing, invest the extra time in Rust.

Authors
Verified by moderation
Moderation
Rust vs JS/TS Binaries: Backend API Performance