The load balancer has chosen an application instance. We are finally close to the code most developers call “the backend,” but the route handler does not receive raw packets. Several layers turn network bytes into the request object our application understands.
Consider a small server:
app.post('/orders', authenticate, validateOrder, createOrder);
This makes it look as if Node simply waits for /orders and calls createOrder. By the time that handler runs, the operating system, Node runtime, HTTP parser, middleware, and router have already done work on the request.
The operating system accepts the connection
The Node process asks the operating system to listen on an address and port. Node is not reading packets directly from the network card. The kernel handles the packets, keeps track of TCP state, orders the bytes, retransmits lost data, applies flow control, and stores incoming data in socket buffers.
When a connection arrives, the kernel associates it with the listening socket. Completed connections wait in an accept queue until the process accepts them. If the process or machine is overloaded, queues can fill before JavaScript runs.
This gives us another boundary to check during an incident. The process may be running and still fail clients because the machine cannot accept connections quickly enough.
Node registers interest instead of waiting
Node uses libuv to work with the operating system’s asynchronous I/O features. It does not block one JavaScript thread for every open connection. Instead, it asks the operating system to notify it when a socket has data to read or when a pending write can continue.
When the operating system reports readiness or completion, Node schedules the corresponding callback work. The JavaScript event loop processes that work in turns.
How a network request moves from the operating system to Node and application code
This model works well for I/O-heavy servers because requests spend a lot of their lifetime waiting on a database, cache, socket, file system, or another service. Node can use that waiting time to run JavaScript for a different request.
Bytes become an HTTP message
TCP gives Node an ordered stream of bytes, not a complete HTTP request in one piece. One read may contain half of a header, one full request, or bytes belonging to several pipelined requests.
Node’s HTTP/1 and HTTP/2 implementations expose different APIs and parse different wire formats. The HTTP/1 implementation incrementally identifies the request line, headers, and body framing; HTTP/2 works with pseudo-headers and frames. Both ultimately expose higher-level request events and objects to application code.
The body may arrive in chunks. This matters for large uploads. If application code buffers every body completely before checking limits, memory use grows with concurrent request size.
app.post('/upload', async (req, res) => {
// A streaming implementation can process chunks incrementally.
// A buffering implementation holds the whole payload in memory.
});
Backpressure tells the side producing data to slow down when the consumer cannot keep up. If the application ignores it, a fast upload and slow processing code can keep filling memory.
Frameworks build a convenient abstraction
Express, Fastify, NestJS, and similar frameworks add structure around Node’s request and response primitives. They may:
- parse JSON and form bodies;
- attach request IDs and logging context;
- reconstruct client information from trusted proxy headers;
- authenticate credentials;
- validate schemas;
- apply rate limits;
- select a route;
- serialize the response;
- translate thrown errors into HTTP responses.
The exact order matters. Authentication that runs after expensive body processing can waste resources. Error middleware registered in the wrong place can miss failures. A logging layer that does not preserve async context can lose correlation between downstream events.
Every middleware function adds work to the request path. A long enough chain will eventually show up in latency, especially when the middleware performs I/O or parses large bodies.
Routing decides which code owns the request
The router compares the HTTP method and normalized path against registered routes. Parameters such as /orders/:id are extracted, and the matching chain runs.
Routing bugs often appear as application problems but have different causes:
- a proxy removed or added a path prefix;
- route registration order caused a broad pattern to match first;
- trailing-slash or case behavior differed from expectations;
- the request used
PUTwhile the application registeredPOST; - a maximum body size rejected the request before the handler.
By the time createOrder starts, the framework has already interpreted much of the request and may have rejected it at several earlier points.
One event-loop turn should remain short
JavaScript callbacks run on the main event-loop thread. A callback continues until it returns or reaches an asynchronous boundary that yields control.
This code blocks other JavaScript work on the same process:
app.post('/orders', (req, res) => {
const result = performHugeSynchronousCalculation(req.body);
res.send(result);
});
While the calculation runs, sockets may continue receiving data in the kernel, but Node cannot promptly execute the callbacks that process it. Latency rises across unrelated requests.
Adding async to the function does not move the calculation to another thread. It only gives us syntax for working with promises. The CPU-heavy code still blocks the event loop until it reaches a real asynchronous operation and yields.
Errors can happen before and after headers
The response is also a stream. Once Node sends response headers, changing the status code is too late. If an error occurs after a partial response, the server may have to close the connection.
The application needs to handle these failures differently:
- validation or authentication failures that can return a clean 4xx response;
- expected dependency failures that map to a deliberate response;
- programming errors that should be logged and isolated;
- process-level failures that may require a restart;
- client cancellations where continuing expensive work has no value.
Every request also needs a deadline. If the client or proxy stops waiting after five seconds while the application continues for a minute, that work keeps using capacity even though nobody can use the response.
What “Node received it” can mean
During debugging, be precise about the observation point:
| Observation | What it proves |
|---|---|
| Packet reached the host | Network delivery reached the machine |
| Kernel accepted the connection | A listening socket and queue were available |
| Node emitted a request | HTTP parsing progressed far enough |
| Request logger ran | The middleware chain began |
| Controller trace began | Routing and earlier middleware completed |
| Business operation committed | The intended state change survived |
Each observation proves that the request reached a different point in the system. To understand where time was spent, we need timestamps and a shared request or trace identifier across enough of those points.
The handler is running now, but JavaScript still has one main thread. The next episode explains how Node uses that thread to keep many requests in progress at the same time.
