Load Test Benchmarks — Go vs NestJS vs Express
Same news platform, built three times, deployed on one shared VPS — real Artillery
load-test results, not a framework listicle.
The Three Backends
- Go — single static binary, PM2-managed, goroutines for concurrency, chi router feels like Express.
- NestJS — Angular-style DI (
@Injectable, decorators, modules).
- Express — dropped the ORM for raw parameterized SQL: full control over every query and index.
Infrastructure
One shared VPS — 2 vCPU, 8GB RAM, 100GB NVMe — with nginx in front, all three backends
(plus 3 unrelated apps) running via PM2 on the same two cores. MySQL for NestJS/Express,
Postgres + pgvector for Go. Go's search is real, working semantic search: a
locally-running Ollama instance (nomic-embed-text) embeds verified articles,
served back via a cosine-distance nearest-neighbor query.
Round 1 — Normal Load
1→20 requests/second ramp over 60 seconds, same endpoint (GET /v1/feed) on all three.
| Backend | p50 latency | Success rate |
| Go | 34ms | 6% |
| NestJS | 39ms | 50% |
| Express | 39ms | 100% |
In plain terms: "p50 latency" is roughly how long you'd typically wait for
someone to pick up the phone — around 34–39 thousandths of a second for all
three, so no real difference in speed. "Success rate" is how many callers got through vs.
got turned away with a "call back later." Go turned away 94 out of 100 callers here, NestJS
turned away half, Express turned away none — but that's not Express being faster or
better. Each backend just has its own house rule for how many requests it accepts before
saying "we're full for now," and Go's rule is simply the strictest of the three, NestJS's in
the middle, Express's the most lenient. The spread above is entirely down to those different
rate limits, not raw speed.
Round 2 — Heavy Load
Ramped to 300 requests/second sustained — well past all three rate limiters.
| Backend | Timeout rate | 429 (fast reject) | 200 OK |
| Go | 98.4% | 1.5% | 0.2% |
| Express | 98.2% | 0% | 1.8% |
| NestJS | 98.5% | 0% | 1.5% |
In plain terms: imagine calling a restaurant to order. "200 OK" = they
picked up and took the order. "429" = they picked up and said "we're slammed, call back
later" — a fast, honest no. "Timeout" = the phone just rings forever, no answer at
all. At 300 calls/second, about 98 out of 100 calls to any of the three got no answer
whatsoever — that's everyone struggling equally, most likely because the test itself
couldn't place that many calls cleanly, not because any one backend is weak. The one
difference: Go still managed to say "call back later" to a few callers under this pressure;
the other two never did — every call to them either connected or rang forever, nothing
in between. Suggestive of Go staying responsive a little further into overload, not proof of
a winner from a single run.
Takeaway
At normal scale, framework speed wasn't the differentiator — deployment simplicity
(Go), team-fit via familiar patterns (NestJS), and control over the data layer (Express)
were. Under extreme, client-saturating load, none of the three "won" outright, though Go
showed the only sign of graceful degradation.