Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Node.js runs JavaScript setup code and callbacks on an event loop, while the operating system and libuv handle much of the work needed to wait for I/O. When an HTTP request arrives, Node’s core HTTP layer parses the message and emits a request event; the attached JavaScript listener runs synchronously. The key distinction is that asynchronous I/O does not make every JavaScript callback asynchronous, and “single-threaded” describes the JavaScript callback path—not every activity in a Node.js process.

What happens when a Node.js program starts?

Node.js evaluates the entry script, loads modules, initializes objects, and registers callbacks. It then enters the event loop automatically; application code does not call a loop-start function. The runtime is designed as an asynchronous, event-driven JavaScript environment for network applications, as the Node.js project explains.

The process can finish when no work remains that keeps it active. While it is running, the event loop coordinates JavaScript callbacks with I/O readiness and completion notifications. A useful way to understand the system is to follow where each kind of work happens rather than imagine a single queue containing everything.

What does “single-threaded” mean in Node.js?

For ordinary application code, JavaScript callbacks run one at a time on the event-loop thread. That does not mean the whole process has only one thread, or that every I/O operation is carried out by JavaScript. Network sockets can use the operating system’s non-blocking I/O mechanisms, and selected operations can be submitted to libuv’s worker pool.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Work or mechanism Where it happens What to remember
JavaScript handler or callback The event-loop JavaScript thread Runs until it returns; a long callback prevents other JavaScript callbacks from progressing.
Socket I/O readiness Operating-system polling coordinated by libuv The OS signals when a socket can be read or written; this is not the same as putting every network operation in the worker pool.
Selected filesystem, DNS, crypto, and zlib work libuv worker pool, for APIs that use it Workers perform the submitted task; completion is reported back so the event loop can continue the JavaScript-side work.

The distinction matters because a program may handle many connections without dedicating a JavaScript thread to each one, but it can still stall if a callback blocks the event-loop thread. The Node.js guide describes both event-loop and worker-pool blocking, and the consequences for throughput, in “Don’t Block the Event Loop (or the Worker Pool)”.

How do the event loop and libuv fit together?

libuv is the cross-platform systems library Node.js uses for event-driven I/O machinery and a worker-pool task interface. Its design overview describes the I/O loop as the central part of libuv. The loop is intended to run on a single thread and uses the best available polling mechanism on the platform to monitor non-blocking sockets.

The operating-system mechanism differs by platform: the Node.js guide names epoll on Linux, kqueue on macOS, event ports on Solaris, and IOCP on Windows. The loop monitors file descriptors or their platform equivalents; when an I/O source is ready, Node.js can invoke the associated callback on the JavaScript path. This is why the event loop is more than a JavaScript callback queue.

The worker pool is a different mechanism. It has queued tasks for work that uses the pool, and a worker’s completion notifies the event loop. Node.js uses the pool for selected APIs, including filesystem operations and some DNS, crypto, and zlib operations. That does not mean all asynchronous network traffic passes through those workers: socket readiness is generally handled through the operating system’s polling facilities.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

libuv documentation distinguishes long-lived handles, such as a TCP server, from shorter-lived requests, such as a write operation. As a mental model, a server handle represents an ongoing source of activity; an individual operation is work initiated against that or another resource. The runtime arranges for readiness or task completion to lead to the appropriate callback.

What happens when an HTTP request reaches a Node.js server?

Node’s built-in node:http module provides HTTP client and server APIs. A server commonly accepts a TCP connection, parses incoming HTTP messages, and emits a 'request' event with an IncomingMessage and a ServerResponse. A keep-alive connection can carry more than one request, so a connection and a request are not necessarily one-to-one.

  1. The socket becomes ready. The operating system’s I/O mechanism reports readiness, and Node’s event-loop machinery dispatches the associated work.
  2. The HTTP layer processes the message. The core module handles message parsing and stream processing, exposing request and response objects to the server code.
  3. The server emits 'request'. Listeners attached to that event are called synchronously as part of emission.
  4. Application code handles the request. It can inspect the method, URL, headers, and streamed body, then write or pipe a response.
  5. Later work completes through callbacks or events. If the handler starts asynchronous I/O, its completion is handled later; that is separate from the synchronous invocation of the request listener.

The HTTP API is intentionally low-level: it parses HTTP message structure and exposes streams, but it does not buffer an entire request or response for you, interpret application-specific headers and bodies, route paths, or automatically parse JSON. Those behaviors belong in your code or a framework layered over the core module. The Node.js v26.10.0 HTTP documentation describes the API and its version-specific behavior.

Because request and response data are streams, server code should consume or pipe request bodies and write responses with stream behavior in mind. A handler should not assume that the complete body is already available when the request event fires.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Are EventEmitter listeners asynchronous?

No. By default, calling emit() invokes the listeners attached to that event synchronously, in registration order. The Node.js v25.9.0 Events documentation states that listeners for an emitted event are called synchronously.

For example, if a server emits 'request', the registered request listener begins running on the current JavaScript callback path. If that listener calls emit() on another emitter, those listeners also run as part of that call unless the code explicitly schedules later work. An event is a way to notify listeners, not a promise that they run asynchronously.

A listener can schedule work for later with a mechanism such as setImmediate(). That changes when the scheduled function runs; it does not change the fact that the original emit() call invokes its listeners synchronously. Likewise, an I/O callback that later emits an event still calls its listeners synchronously at the point of emission.

Why the 'error' event needs deliberate handling

'error' is a special EventEmitter event. If an emitter emits it without a registered error listener, Node.js throws the error, which can cause the process to exit. If your application needs to handle errors from an emitter, attach an intentional error listener and decide how that error should be handled; EventEmitter itself is not an error-recovery system.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

What can delay other requests or callbacks?

When a JavaScript callback performs a long synchronous computation, no other JavaScript callback can run on that event-loop thread until the computation returns. A CPU-heavy loop in one request handler can therefore delay callbacks for unrelated connections. This is the practical limit behind the claim that Node.js is well suited to many I/O-bound workloads: each callback still needs to finish promptly enough for the loop to make progress.

Worker-pool work has its own congestion risk. A long task occupies a worker while other submitted tasks wait for an available worker. Moving work to the pool avoids doing that task on the event-loop JavaScript thread, but it does not make worker capacity unlimited or remove the cost of the work.

  • Keep event-loop callbacks bounded; avoid long synchronous computation in request handlers.
  • For suitable small computations, divide the work into bounded pieces so other callbacks can make progress between them.
  • For substantial CPU-heavy work, consider an appropriate separate worker or process. Communication with workers involves data transfer, including serialization and copying, rather than sharing the event loop’s ordinary JavaScript object namespace.
  • Consider the workload before choosing Node.js: it is designed to support I/O-bound applications, but the guide cautions that it may not be the best fit for programs dominated by expensive calculations.
  • Treat unexpectedly expensive work as a resilience concern: inputs that trigger long processing can delay service for other clients.

None of these mechanisms guarantees that adding workers will improve every application. The result depends on where the workload spends time, whether work blocks the event loop or occupies the pool, and the costs of dividing and communicating the work.

How precise are Node.js timers?

A call such as setTimeout(callback, delay) asks Node.js to schedule a callback after a requested delay; it is not a deadline. The v26.10.0 Timers documentation says callbacks are called as close as possible to the requested time and does not guarantee an exact firing time or a particular ordering relative to other callbacks. If the event loop is busy, a timer callback can run later than requested.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

A compact mental model

  • JavaScript: initialization and callbacks execute on the event-loop path, one at a time.
  • Socket I/O: the OS and libuv monitor readiness; this is distinct from worker-pool task execution.
  • Selected tasks: APIs may submit work to libuv’s pool, with completion later reported to the loop.
  • HTTP: the core module parses message structure and exposes streams, then emits request events for application code.
  • Emitters: listeners run synchronously when an event is emitted, unless the code separately schedules later work.

Keeping those paths separate makes the apparent paradox disappear: Node.js can wait efficiently for many I/O operations, but JavaScript handlers do not run in parallel on the event-loop thread, and an event listener is not asynchronous just because it is called a listener.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.