← Back

article / typescript node revision

How Node Handles Concurrent Requests

To understand a concurrent server, follow one request across its boundaries. The operating system transports bytes, Node interprets a protocol and invokes application code, a database driver tracks an operation, and a promise connects its eventual result to a suspended handler. None of those steps requires an automatic JavaScript worker thread for each request.

The useful question is not simply “is this asynchronous?” Ask which component owns the work, what state it retains, and what event permits the next step. This makes both scalability and correctness visible. Efficiently waiting for a database response does not guarantee that a later read sees an earlier write, just as a socket becoming readable does not guarantee that a complete HTTP body has arrived.

Connections and request lifecycle

Start with a basic Node HTTP server:

const http = require("node:http");
const server = http.createServer((req, res) => {
  res.end("hello");
});
server.listen(8080);

listen(8080) asks the operating system to establish a listening endpoint for the server. The kernel owns the networking stack and socket state. Node does not directly inspect every network packet arriving at the machine, and port 8080 does not describe one permanently occupied client connection.

A listening socket accepts new connections. Each accepted TCP connection has a connected socket with its own transport state, including receive and send buffers. The server endpoint can remain the same across thousands of connections because each connection is distinguished by its client and server endpoints. In the simplified TCP model here, that identity is a four-tuple: client IP, client port, server IP, server port.

10.0.0.1:51001 → 20.0.0.1:8080
10.0.0.1:51002 → 20.0.0.1:8080
10.0.0.2:51001 → 20.0.0.1:8080

Even one client machine can establish several connections by using different source ports. A port identifies the server endpoint; the complete tuple identifies a connection. When bytes arrive, the OS associates them with the correct connected socket. Application code receives a connection whose identity is already established, rather than repeatedly guessing which client sent a packet. Node exposes listening servers and connected sockets through its TCP networking API.

Practice: Can 10,000 TCP connections share port 8080? If 9,999 are idle, does Node need a waiting JavaScript thread for each?

Reveal answer

Yes, distinct endpoint tuples and connected-socket state distinguish them. No dedicated JavaScript waiting thread is needed for each socket; OS network facilities report relevant activity. File descriptors, buffers, memory and other resource limits still constrain the real number of connections.

From readable bytes to an application request

Suppose the client sends POST /pay, with bytes arriving in three chunks: an initial request line, headers plus part of the body, then the remaining body. TCP gives the application an ordered byte stream. It does not preserve these arrival chunks as three application messages, and the OS does not need to know what /pay means.

There are three interpretations at different layers. The OS handles the transport connection and makes bytes available. Node's HTTP machinery parses those bytes into HTTP structure. The application or framework interprets the route and consumes the body. “Socket readable” only says that reading can make progress; it is not a complete-request notification.

For HTTP/1, Node can invoke a request handler after parsing the headers while the body continues arriving as a stream. A framework's body-parsing middleware may wait for and decode that body before invoking your route. This is why req.body in a framework example must not be mistaken for a property Node's raw HTTP server automatically fills. Node's HTTP API defines the request as a readable stream.

Practice: Who reconstructs a three-chunk HTTP request? Must the entire body have arrived when a raw Node request callback begins?

Reveal answer

Node's HTTP parser interprets the stream; the application consumes the body and any middleware can decode it. The kernel handles TCP, not HTTP routes or bodies. No: a request callback can run after headers are parsed while the body is still arriving.

From the handler to the database and back

Now suppose request A reaches a route that queries a network database. Its JavaScript executes on the shared event-loop thread, exactly like an ordinary function call:

async function handler(req, res) {
  const user = await database.query("SELECT ...");
  res.end(JSON.stringify(user));
}

The query string is schematic: the driver's real API and result shape determine the details. The important dependency is the promise. The driver creates state for the outstanding operation, writes protocol bytes to its database connection, and returns a promise representing the result. A suspends at the await. Its retained state includes what is needed to resume and produce the response, without a dedicated thread waiting at the source line.

While the database server computes the query, the Node thread can process request B, a timer, or other runnable work. Database computation occurs in the database process; it does not occupy a hidden Node request worker. When response bytes arrive, the kernel identifies the database socket. The driver parses the database protocol and associates the result with the correct outstanding operation according to that protocol and its own queueing state.

That is a second correlation problem: the OS maps bytes to a connection; the driver maps a protocol response to an operation. Some protocols serialize operations, and some support multiplexing. We need not assume every response carries an arbitrary request ID to understand the principle. The corresponding promise settles, scheduling A's continuation onto the same JavaScript thread.

Client → connected socket → HTTP parser → handler A
                                         ↓ query promise
                                   database connection
                                         ↓ response bytes
                               driver settles the promise
                                         ↓ continuation
                                  handler A → response

There is no separate “database call stack” alongside an “HTTP call stack.” There are several sources of runnable work and one ordinary JavaScript execution stack in this model. If B is currently doing a synchronous calculation, A cannot interrupt it. The scheduling rules in How Asynchronous JavaScript Executes determine when A actually resumes.

Practice: A database query takes five seconds; B arrives one millisecond after A suspends. Can B run? When the result arrives, does Node restore a dedicated A stack?

Reveal answer

B can run while A waits, provided other work does not block the thread. A has retained continuation state, not a reserved waiting thread or physical request stack. Its continuation later executes as new work on the same shared thread.

Ordering and the meaning of acknowledgement

Concurrency permits overlapping waits, which means arrival order alone does not create a business dependency. If A writes a user's name and B reads it, B can complete before A commits. Ordered bytes on one TCP connection do not establish the database ordering you require. HTTP/1 response ordering does not imply that business operations cannot overlap, and HTTP/2 can multiplex request streams over one connection.

The application must express dependencies where they matter. Independent operations can overlap; a read that depends on an update must await the completion contract of that update:

// Independent operations can overlap.
const [profile, orders] = await Promise.all([getProfile(), getOrders()]);

// Dependent operations need a completion contract and sequencing.
await updateUser();
const user = await getUser();

The second sequence only helps if updateUser() resolves after the promised update is actually complete. An HTTP response can acknowledge receipt, enqueueing, or committed work; the status code alone cannot tell us which contract the server implemented.

Practice: A framework route contains the following code. Can a GET sent after receiving 200 still observe the old value?

app.post("/user", async (req, res) => {
  database.write(req.body); // intentionally not awaited
  res.sendStatus(200);
});
Reveal answer

Yes. The response can be sent while the write is outstanding, and the write might subsequently fail. The client's wait for 200 only waits for that premature acknowledgement. Await the operation and handle failures before acknowledging the completion your endpoint promises.

A corrected outline first awaits the write and then responds. But adding await does not strengthen the database API itself. If its promise represents submission to a queue rather than committed work, the contract remains too weak. Durability and visibility also depend on transaction settings and where the subsequent read runs. PostgreSQL, for example, documents that asynchronous commit can acknowledge a transaction before its WAL reaches durable storage; replica visibility is a further read-consistency concern. Specify the required guarantee instead of equating “promise fulfilled” with “durable everywhere.”

Transport completion is another boundary. Node's response finish event means its response has been handed to the OS for transmission, as defined in the HTTP response documentation. It does not prove that the client has received it, and it says nothing about a database commit.

Practice: Does res.end() or the response's finish event prove client receipt or persistence of the write?

Reveal answer

No. Ending an HTTP response, handing bytes to the OS, client receipt, and database durability are separate milestones. Only an explicitly defined operation contract establishes the persistence guarantee.

OS readiness and the worker pool

We have followed the request logically; now locate the machinery that makes the waits efficient. The kernel owns TCP/IP, socket buffers and platform notification facilities. libuv's event loop lives in user space inside Node and translates relevant OS activity into opportunities for Node to process I/O and invoke callbacks.

On Linux, the model uses readiness facilities such as epoll; other platforms use their own mechanisms. The loop can wait for activity across registered sockets rather than repeatedly busy-checking 10,000 idle connections. Efficiently sleeping inside an OS wait when no application work is runnable is different from monopolizing JavaScript with a busy loop. One avoids wasting CPU until an event occurs; the other prevents eligible callbacks from executing.

Normal network I/O does not reserve a libuv worker to sit waiting for every response. A database connection typically follows the same socket path as other network traffic. Query promises retain application state while the OS handles transport. Having 10,000 outstanding queries therefore does not necessarily imply 10,000 waiting Node threads. Actual database connection pools, server capacity, pending-operation memory and buffers remain important limits.

Other asynchronous operations use a different route. libuv's bounded worker pool performs work including asynchronous filesystem operations and some DNS, cryptography and compression APIs. dns.lookup can use that pool even when it is part of establishing a network connection; not every DNS API uses it. “Network operation” is consequently not a promise that every preceding step avoids workers. The distinction is documented in Node's event-loop and worker-pool guide.

A hundred concurrent fs.promises.readFile calls submit filesystem work against a bounded number of workers. Pending tasks queue rather than creating a hundred JavaScript request threads. On completion, JavaScript callbacks and promise continuations still return to the event-loop thread. Saturating the pool can delay unrelated operations that share it while the JavaScript thread itself remains responsive.

Socket data → kernel notification → libuv/Node → driver → JS continuation
File task   → worker-pool execution → completion notification → JS continuation

Practice: Do 100 simultaneous asynchronous file reads create 100 request threads? Why do files use workers while sockets use OS notification?

Reveal answer

No. The reads use the bounded worker pool and can queue for available workers. Verified editorial answer to the source's final unanswered question: libuv implements asynchronous filesystem operations through its pool because it lacks a portable file-I/O notification model equivalent to its network facilities. This describes libuv's design, rather than claiming no operating system has asynchronous file APIs. See the libuv design overview.

Finally, the worker pool is not an automatic destination for arbitrary JavaScript in a handler. A CPU-heavy calculate() still occupies the JavaScript thread unless the application explicitly moves that work to a worker or process, or divides it into segments that yield appropriately. Keeping these responsibilities separate lets us reason about both failure modes: a blocked execution thread delays runnable handlers, while an overloaded pool delays operations awaiting worker capacity.

Source: TypeScript-Inference-Practice.pdf, pp. 33–54, 61. Supporting references clarify streaming bodies, completion guarantees and platform boundaries; the final unanswered question's verified answer is labeled above. Back to TypeScript and Node Revision.