What Really Happens When 10,000 Requests Hit a Node.js Server
What Really Happens When 10,000 Requests Hit a Node.js Server
Most engineers think Node.js “handles 10,000 requests concurrently.”
It doesn’t.
What actually happens is far more interesting — and far more dangerous if you misunderstand it.
When a traffic spike hits your service, Node.js is only one piece of the story.
The real system includes:
- the OS kernel
- socket queues
- epoll/kqueue
- the event loop
- downstream dependencies
This article breaks down the full lifecycle of those 10,000 requests — and where things actually break.
The Problem
You deploy a Node.js service.
It works fine under normal load.
Then traffic spikes:
- 10,000 incoming requests in seconds
- latency jumps
- some requests time out
- some never reach your app
CPU usage?
Still low.
This is where most engineers get confused.
Why It Happens
Node.js is not a thread-per-request model.
Instead:
- One thread runs the event loop
- The OS handles incoming connections
- Node reacts only when sockets are ready
This is called a readiness-based I/O model.
Key implication:
Node.js does not process all requests instantly — it processes them when the event loop gets to them.
Internal Architecture
Let’s follow a request from the network to your code.
Step 1: Incoming Connections Hit the Kernel
Before Node.js sees anything, the OS is already buffering connections.
flowchart LR
Client --> NIC[Network Interface]
NIC --> Kernel
Kernel --> SocketQueue[Socket Accept Queue]
SocketQueue --> Node
The kernel maintains:
- SYN queue (half-open connections)
- accept queue (fully established connections)
If these queues fill up:
- new connections are dropped
- clients see timeouts or connection resets
Node.js is not even involved yet.
Step 2: Node.js Uses epoll/kqueue
Node doesn’t poll sockets manually.
It asks the OS:
“Tell me when something is ready.”
This is done via:
epoll(Linux)kqueue(macOS/BSD)
flowchart TD
Kernel -->|ready events| epoll
epoll --> EventLoop
EventLoop --> JSHandlers
Important detail:
- epoll returns ready file descriptors
- not “requests to process”
- not “tasks to execute”
Node still has to:
- accept the connection
- read data
- parse HTTP
- run your handler
Step 3: Event Loop Processes Requests Sequentially
Here’s the critical bottleneck.
sequenceDiagram
participant C1 as Client 1
participant C2 as Client 2
participant EL as Event Loop
C1->>EL: Request
C2->>EL: Request
EL->>EL: Handle C1
EL->>EL: Handle C2
Even with 10,000 connections:
- only one callback executes at a time
- no parallel JS execution
Concurrency ≠ parallelism.
Example Scenario
Let’s simulate reality.
Case 1: Fast Handler (Ideal Case)
app.get("/", (req, res) => {
res.send("ok");
});
- event loop is free quickly
- can cycle through thousands of requests
- high throughput
Case 2: Slow Downstream Call
app.get("/", async (req, res) => {
const data = await db.query("SELECT * FROM users");
res.send(data);
});
Now:
- each request waits on DB
- sockets remain open
- responses pile up
flowchart LR
Requests --> Node
Node --> DB
DB --> SlowResponse
SlowResponse --> Backlog
This creates:
- connection buildup
- memory pressure
- increased latency
Case 3: Blocking Code (Worst Case)
app.get("/", (req, res) => {
const data = JSON.parse(hugeString); // blocks event loop
res.send(data);
});
Now:
- event loop is blocked
- all 10,000 requests stall
Where Things Break
1. Kernel Queue Overflow
If traffic > accept capacity:
- SYN queue fills
- accept queue fills
- connections dropped
No logs in Node.
Just lost traffic.
2. Event Loop Saturation
If handlers are slow:
- event loop lag increases
- requests wait longer
- tail latency explodes
3. Downstream Backpressure
If DB or APIs are slow:
- open connections accumulate
- memory usage grows
- retries amplify load
4. File Descriptor Limits
Each connection = 1 file descriptor.
Limits:
ulimit -n- container limits
Hit the limit → server stops accepting connections.
Diagram: End-to-End Flow
flowchart LR
Clients --> KernelQueue
KernelQueue --> epoll
epoll --> EventLoop
EventLoop --> AppLogic
AppLogic --> DB
DB --> AppLogic
AppLogic --> EventLoop
EventLoop --> Clients
Key Takeaways
- Node.js does not process all requests simultaneously
- The OS kernel is your first bottleneck
- epoll gives readiness, not execution
- Event loop is the true throughput limiter
- Slow downstreams create hidden queues
- Blocking code freezes everything
Practical Debugging Tips
When your system struggles under load:
Measure the right things
- event loop lag (p95, p99)
- open connections
- socket queue drops (
netstat -s) - file descriptor usage
Tune the system
- increase backlog (
server.listen(backlog)) - raise
ulimit -n - tune kernel params (
somaxconn)
Protect the system
- implement timeouts
- use circuit breakers
- apply backpressure
Final Thoughts
Node.js scales incredibly well — but only if you understand where the real limits are.
The biggest mistake engineers make is thinking:
“Node handles concurrency automatically.”
It doesn’t.
Concurrency is a system-level property:
- kernel queues
- event loop scheduling
- downstream capacity
If you ignore any one of these, your system will fail — silently and unpredictably.
The next time someone says:
“Node can handle 10,000 requests easily”
You’ll know the real answer:
“It depends — on everything around Node.”
Next in the Series
Continue reading: From Socket to Callback — The Hidden Journey of Every Node.js Request