Why Most Comparisons Are Useless
Search "Go vs Node.js performance" and you get one of two things: opinion posts from engineers who chose one and are justifying it, or microbenchmarks measuring raw arithmetic that tell you nothing about real API workloads.
This post benchmarks something closer to reality: a REST API that reads from a database, serializes a response, and handles concurrent connections. Not a loop counting to a billion.
Test Setup
We built identical APIs in both languages. Same route structure, same PostgreSQL query, same JSON response shape.
The API under test
A single endpoint: GET /users/:id
- Connects to PostgreSQL (local Docker container, same host)
- Runs a primary key lookup:
SELECT id, name, email, created_at FROM users WHERE id = $1 - Returns a JSON object (4 fields)
- No caching layer, no application-level pooling tricks beyond the standard library
Go implementation
Chi router, pgx/v5 connection pool (max 25 connections), encoding/json for serialization.
Node.js implementation
Fastify (not Express — Fastify is faster), pg connection pool (max 25 connections), default JSON serializer.
Load testing tool
wrk with the following parameters:
- Duration: 30 seconds per run
- Threads: 4 (matches the test machine's cores)
- Connections: 100, 500, and 1000
- Warmup: 10 seconds before each timed run (not included in results)
Test machine
AWS EC2 t3.medium (2 vCPU, 4 GB RAM), Ubuntu 22.04. Same instance for both services, restarted between runs. PostgreSQL on the same instance to remove network latency as a variable.
Results
100 concurrent connections
| Metric | Go (Chi) | Node.js (Fastify) |
|---|---|---|
| Requests/sec | 14,820 | 11,340 |
| Avg latency | 6.7 ms | 8.8 ms |
| p99 latency | 18 ms | 31 ms |
| Memory (RSS) | 22 MB | 68 MB |
| Errors | 0 | 0 |
500 concurrent connections
| Metric | Go (Chi) | Node.js (Fastify) |
|---|---|---|
| Requests/sec | 13,980 | 10,110 |
| Avg latency | 35 ms | 49 ms |
| p99 latency | 89 ms | 187 ms |
| Memory (RSS) | 38 MB | 94 MB |
| Errors | 0 | 3 (timeout) |
1000 concurrent connections
| Metric | Go (Chi) | Node.js (Fastify) |
|---|---|---|
| Requests/sec | 12,740 | 8,350 |
| Avg latency | 78 ms | 119 ms |
| p99 latency | 210 ms | 580 ms |
| Memory (RSS) | 61 MB | 148 MB |
| Errors | 0 | 27 (timeout) |
What the Numbers Actually Mean
Go's throughput advantage is real but not massive at low concurrency
At 100 connections, Go handles 31% more requests per second than Fastify. This is meaningful but not the "10x" figure you see in blog posts that measure raw HTTP handlers with no database. The database becomes the bottleneck at both ends before the framework overhead matters much.
The p99 latency gap widens significantly under load
At 1000 connections, Go's p99 is 210ms. Node.js's is 580ms. If you have SLAs on tail latency, that gap matters. Node.js's single-threaded event loop starts showing stress when connection queues build up, even with Fastify's optimized HTTP handling.
Memory is where Go wins decisively
At peak load, Go's process used 61 MB RSS. Node.js used 148 MB. On a t3.medium you can run 3-4× more Go replicas for the same memory cost. At scale, this translates directly to infrastructure cost.
Errors under extreme load
Go produced zero errors at all concurrency levels. Node.js produced 27 timeouts at 1000 connections, all on the pg connection pool queue. This is a pg pool behavior under queue saturation, not a fundamental Node.js limitation, but Go's goroutine model handles this more gracefully out of the box.
Where Node.js Still Wins
Raw throughput is not the only axis that matters for an engineering decision. Node.js has real advantages:
- Ecosystem: npm has 3 million packages. Go has fewer, especially for integration-heavy work (payments, CRMs, third-party APIs).
- TypeScript full-stack sharing: If your frontend is React/Next.js, sharing types between client and server eliminates a whole category of bugs. Go cannot do this.
- Hiring: There are 10× more Node.js engineers than Go engineers. For most startups, this matters more than p99 latency.
- Developer velocity: For CRUD-heavy applications, Node.js with Prisma or Drizzle is faster to ship than Go with manual SQL.
When We Choose Go Over Node.js
Based on the numbers and our production experience:
- The API needs to handle sustained concurrency over 500 req/s on small instances
- Tail latency SLAs exist (p99 under 200ms)
- The service runs as an AWS Lambda function (Go cold starts are 10-30ms vs 200-400ms for Node.js)
- Infrastructure cost matters and the service runs at high volume
- The team already has Go experience
When We Choose Node.js
- The application is a CRUD API with moderate traffic
- TypeScript type-sharing with a React frontend is valuable
- Speed of development is the primary constraint
- The team is JavaScript-first
- Deep npm ecosystem access is needed (third-party integrations)
The Honest Answer
Most applications will not hit the concurrency levels where these differences are a business-critical factor. If you are building an internal tool, a startup MVP, or a moderate-traffic API, both Go and Node.js with Fastify will serve you well. Choose based on team expertise and ecosystem fit.
If you are building a high-throughput service, a latency-sensitive API, or a Lambda-heavy architecture, the numbers above are a real part of the decision.
We have production experience with both. If you need a second opinion on your stack choice, describe what you are building and we will give you a direct answer.