Choosing a GraphQL server for Node.js? I benchmarked Apollo, Mercurius and Yoga on Express, Fastify and plain node:http, each with and without NestJS: 10 setups, same schema, same queries.
This is a follow-up to my NestJS GraphQL benchmark (Express vs Fastify, Apollo vs Mercurius). The most common question there was: "How much does NestJS itself cost? What if I drop it?" So this time I added:
- NestJS + GraphQL Yoga
- The same stacks without NestJS: Express + Apollo, Fastify + Apollo, Fastify + Mercurius, Fastify + Yoga
-
Yoga on Express and Yoga standalone on plain
node:http
…and re-ran everything.
TL;DR
- The GraphQL engine matters most. Mercurius did 1.3–1.8× the throughput of Apollo in every query scenario, with about half the p99 latency on nested queries. Yoga landed in between on real queries.
-
The HTTP framework matters mostly for tiny queries. With Apollo, Fastify beat Express by 18–29% on
healthbut only ~3% ondeep-nested. For Yoga, standalone, Express and Fastify were within a few percent on real queries. - NestJS adds no measurable overhead. Each NestJS server is within −6% to +5% of the same stack without it, and which one wins flips from scenario to scenario.
- Mutations are the same everywhere (~9,000–9,400 req/s).
📊 Interactive dashboard: https://gql-bench.vercel.app/
💻 Code (run it yourself): https://github.com/vovadyach/gql-bench
What was tested
Every server implements the same schema, data and resolvers. Only the stack changes.
| NestJS 10 | Direct (no NestJS) |
|---|---|
| Express + Apollo | Express + Apollo |
| Fastify + Apollo | Fastify + Apollo |
| Fastify + Mercurius | Fastify + Mercurius |
| Fastify + Yoga | Fastify + Yoga |
| Express + Yoga | |
Yoga standalone (node:http) |
Scenarios:
| Scenario | What it is |
|---|---|
health |
Tiny query: pure framework + GraphQL engine overhead |
single-user |
1 user + posts + comments |
user-list |
10 users, nested |
deep-nested |
5 × 10 × 8 nested objects + some CPU work (~90 KB responses) |
mutation |
Write with body parsing |
| Mixed traffic | Production-like query mix: 30% health, 25% single-user, 20% mutation, 15% user-list, 10% deep-nested |
Results
Requests per second, median of 3 full runs (higher is better):
| Server | health | single-user | user-list | deep-nested | mutation | mixed |
|---|---|---|---|---|---|---|
| NestJS Fastify + Mercurius | 30,631 | 13,047 | 4,397 | 902 | 9,065 | 6,862 |
| Direct Fastify + Mercurius | 29,489 | 12,889 | 4,226 | 887 | 9,004 | 6,769 |
| Direct Yoga standalone | 20,994 | 11,050 | 3,562 | 731 | 9,072 | 5,436 |
| Direct Express + Yoga | 19,575 | 11,109 | 3,625 | 730 | 9,162 | 5,388 |
| Direct Fastify + Yoga | 17,854 | 10,525 | 3,548 | 731 | 8,988 | 5,214 |
| NestJS Fastify + Yoga | 16,966 | 10,084 | 3,533 | 753 | 8,984 | 5,151 |
| NestJS Fastify + Apollo | 21,937 | 10,191 | 2,831 | 493 | 9,444 | 3,974 |
| Direct Fastify + Apollo | 21,075 | 9,560 | 2,781 | 484 | 9,423 | 4,055 |
| NestJS Express + Apollo | 17,032 | 9,080 | 2,703 | 480 | 9,378 | 3,706 |
| Direct Express + Apollo | 17,856 | 9,036 | 2,706 | 472 | 9,145 | 3,832 |
p99 latency on deep-nested: 68 ms for Mercurius vs 123 ms for Fastify + Apollo.
NestJS vs direct, pair by pair
Throughput of the direct version relative to the NestJS version:
| Pair | health | single-user | user-list | deep-nested | mutation | mixed |
|---|---|---|---|---|---|---|
| Express + Apollo | +4.8% | −0.5% | +0.1% | −1.8% | −2.5% | +3.4% |
| Fastify + Apollo | −3.9% | −6.2% | −1.8% | −1.9% | −0.2% | +2.0% |
| Fastify + Mercurius | −3.7% | −1.2% | −3.9% | −1.6% | −0.7% | −1.4% |
| Fastify + Yoga | +5.2% | +4.4% | +0.4% | −3.0% | 0.0% | +1.2% |
No consistent winner. NestJS adds dependency injection and a resolver layer, but once a request reaches the GraphQL engine, that layer costs about as much as run-to-run noise.
Methodology
- Apple M1 Max, Node.js 24, single process per server, one server at a time
- k6, ramping to 50 virtual users, 30 s sustained load per scenario
- 3,000 warmup requests per server (discarded), 5 s cooldown between servers
- Every GraphQL response validated; response sizes verified identical across servers
- All runs passed thresholds (<1% HTTP failures, <1% GraphQL errors)
- 3 full runs, median reported
Limits: in-memory data (no database), one machine, single process. Absolute numbers will differ on your hardware; the ratios between servers are the useful part. With a real database, these differences shrink because I/O dominates.
So what should you use?
Not using NestJS?
- Fastest: Fastify + Mercurius, in every query scenario.
- Like Yoga? Its HTTP layer barely matters on real queries: standalone, on Express or on Fastify all landed within a few percent.
- On Apollo? Prefer Fastify over Express for light queries, and expect Apollo to fall behind as queries get more nested.
Using NestJS?
-
Need more throughput? Switch the driver to
@nestjs/mercuriuson Fastify; it gave the biggest gain in every query scenario.
Top comments (0)