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?
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
Context
What we tested
- 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.
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
- 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.
What we conclude
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.