One Request, End to End · Episode 04

What actually happens when Node receives the request?

A secure request arriving at backend application servers
Episode 04Inside Node
Episode 04 of 12Series roadmap

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.

Node request handoffwho owns the work?
The JavaScript thread runs the application steps. It does not sit blocked while the operating system waits for ordinary socket I/O.

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 PUT while the application registered POST;
  • 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.

Sources and further reading