FREN

Let’s keep in touch

Social

Lab registerEXP-02 · Performance
ConclusiveEXP-02iones labUpdated on 20 September 2026

Node, Go, Rust, PHP: the same API, four runtimes, the numbers

The questionAt what load level does the choice of runtime become the limiting factor of a conventional business API?

Verdict

On an API with a database, the gap between runtimes is 1 to 4 in raw throughput, but the database saturates well before that. The runtime only becomes the bottleneck beyond 3,000 requests per second per instance, a threshold most of our clients do not come close to.

DecisionPHP 8.3 and Node stacks kept as the foundation. Go retained for high-concurrency workers. Rust ruled out for business development.

Status
Conclusive
Duration
3 weeks
Team
3 developers, 1 architect
Stack
  • Node 22
  • Go 1.23
  • Rust (Axum)
  • PHP 8.3
  • FrankenPHP
  • Postgres
  • k6
38,400req/s · Rust, no databaseGo 31,200 · Node 12,900 · PHP 9,800
4,100req/s · all runtimes, with databaseceiling set by Postgres
×3.2development time Rust vs PHPon the same scope
41 MBmemory per Go instanceRust 18 · Node 96 · PHP 118
01

Context

“We should rewrite this in Go.” The sentence comes up on every project that slows down. It is sometimes right, often expensive, and rarely backed by a measurement. We wanted numbers of our own, on an API that resembles the ones we deliver: authentication, validation, database reads and writes, JSON serialisation. Not a “hello world”.
02

What we tested

An order management API with 6 endpoints (paginated list, detail, creation with validation, update, search, export) implemented four times by four different developers, each in the runtime they know best, with the same Postgres database and the same dataset of 2 million rows.
  • Node 22 with Fastify.
  • Go 1.23 with the standard library and pgx.
  • Rust with Axum and sqlx.
  • PHP 8.3 with Symfony, served by FrankenPHP in worker mode.
Three load scenarios with k6, on an identical 4 vCPU machine: JSON parsing and serialisation without a database, reads with a database, and a realistic mixed scenario (80% reads, 20% writes). We measured throughput, p50 and p99 latency and memory, and timed the development of each implementation.
03

Results

38,400
req/s · Rust · no database
31,200
req/s · Go · no database
12,900
req/s · Node · no database
9,800
req/s · PHP · no database
Without a database, the expected hierarchy is confirmed: Rust and Go far ahead, Node and PHP at a comparable level. The 1-to-4 gap is real, and memory follows the same curve.
With a database, everyone ends up in the same place. On the mixed scenario, the four implementations plateau between 3,800 and 4,100 requests per second: it is Postgres that saturates, on the same queries and the same indexes. The p99 latency stays under 45 ms for Go and Rust, 70 ms for Node, 85 ms for PHP, which is invisible to a user over a network.
  • FrankenPHP in worker mode changes the picture for PHP: three times more throughput than classic PHP-FPM on the same code, with no application changes.
  • Development time: PHP 11 h, Node 13 h, Go 19 h, Rust 35 h for the same scope, tests included. Rust needed two more reviews to be considered clean.
  • Below 1,000 requests per second, no runtime can be told apart from another on perceived latency.
  • Go stands out on high-concurrency background tasks (export, batch processing): 40 MB of memory and a concurrency model that is easy to review.
04

What we conclude

Changing runtime for performance only makes sense if the runtime is genuinely the bottleneck, and on a conventional business API, it almost never is. The database, the queries, the cache and the network come well before it. A rewrite costs weeks and makes the team lose expertise on its own code; a missing index is fixed in an hour.
That does not mean the choice is neutral. Go offers a real advantage on workers and heavily used services, at a reasonable development cost. Rust is excellent, but its extra development cost is not justified on ordinary business logic. PHP 8.3 with FrankenPHP remains a perfectly legitimate foundation, and it is the one on which our teams are most productive.
05

What’s next

  • FrankenPHP has become our recommended configuration for new PHP projects and for migrations from PHP-FPM.
  • Go is our default choice for processing workers (exports, queues, integrations) when the volume justifies it.
  • Rewrite requests made for performance reasons now go through prior profiling, with this benchmark as the reference.