From Socket to Callback — The Hidden Journey of Every Node.js Request

From Socket to Callback — The Hidden Journey of Every Node.js Request

Most engineers think Node.js “handles” a request the moment it arrives.

It doesn’t.

Before your handler runs, that request travels through the kernel, sits in queues, waits for readiness signals, and only then enters JavaScript land.

Understanding this journey is what separates surface-level debugging from real production engineering.


The Problem

When latency spikes or requests start dropping, engineers often look at:

  • CPU usage
  • memory
  • application logs

But the real bottleneck is often before your code even runs.

Requests can be:

  • stuck in the OS accept queue
  • delayed in the event loop
  • blocked during response writes

Without understanding the full lifecycle, debugging becomes guesswork.


Why It Happens

Node.js is built on a non-blocking, event-driven architecture powered by libuv.

It does not:

  • create a thread per request
  • immediately execute incoming requests

Instead, it relies on:

  • OS-level socket queues
  • readiness-based I/O (epoll/kqueue)
  • an event loop that schedules execution

This means every request must earn its turn to execute.


Internal Architecture

Let’s break down the journey step by step.


1. Incoming Request → OS Accept Queue

When a client sends a request:

  • the TCP connection is handled by the OS
  • it is placed into the accept queue of the listening socket

If this queue fills up:

  • new connections are dropped
  • Node.js never sees them

2. Node.js Accepts the Connection

Node (via libuv):

  • calls accept() on the socket
  • converts the connection into a file descriptor
  • registers it with the event loop

Now the request is known to Node — but still not processed.


3. Event Loop → Poll Phase (epoll/kqueue)

Node uses:

  • epoll (Linux)
  • kqueue (macOS/BSD)

These systems notify Node when:

  • data is ready to read
  • a socket is ready to write

The event loop’s poll phase:

  • waits for these readiness events
  • queues callbacks when data arrives

4. Callback Scheduling

Once data is ready:

  • Node parses the HTTP request
  • schedules your handler callback

But execution depends on:

  • event loop availability
  • other pending tasks

If the loop is busy:

  • your request waits → event loop lag

5. Handler Execution (Your Code)

Finally:

  • your JavaScript handler runs
server.on('request', (req, res) => {
  res.end("Hello World");
});

At this point:

  • the request has already gone through multiple layers of scheduling

6. Response Write Flow

Sending a response is also non-trivial.

  • res.write() → writes to internal buffer
  • Node attempts to flush to socket
  • OS TCP buffer takes over

If the client is slow:

  • buffers fill up
  • writes become backpressured

Example Scenario

Let’s say your service gets a sudden spike:

  • 10,000 incoming requests
  • slow downstream database
  • large response payloads

What happens?

  1. OS accept queue fills → some requests dropped
  2. accepted connections wait in event loop
  3. slow DB → handlers take longer
  4. event loop backlog increases
  5. response buffers fill due to slow clients
  6. TCP backpressure slows everything further

Even if CPU is low, latency explodes.


Diagram

End-to-End Request Lifecycle

flowchart LR
    Client --> Kernel[OS Kernel TCP Stack]
    Kernel --> AcceptQueue[Accept Queue]
    AcceptQueue --> NodeAccept[Node accept]
    NodeAccept --> EventLoop[Event Loop]
    EventLoop --> PollPhase[Poll Phase epoll/kqueue]
    PollPhase --> CallbackQueue[Callback Queue]
    CallbackQueue --> JSHandler[User Handler]
    JSHandler --> ResponseWrite[res.write]
    ResponseWrite --> TCPBuffer[OS TCP Buffer]
    TCPBuffer --> Client

Backpressure Flow

flowchart LR
    JSHandler --> WriteBuffer[Node Write Buffer]
    WriteBuffer --> TCPBuffer[Kernel TCP Buffer]
    TCPBuffer --> Client

    Client -.slow read.-> TCPBuffer
    TCPBuffer -.fills up.-> WriteBuffer
    WriteBuffer -.blocks writes.-> JSHandler

Key Takeaways

  • A request does not immediately reach your handler
  • The OS accept queue can drop requests before Node sees them
  • Event loop scheduling determines when your code runs
  • Slow handlers increase system-wide latency
  • Response writing can block due to TCP backpressure
  • Bottlenecks often exist outside Node.js, in the kernel or network

Final Thoughts

Senior engineers don’t just debug “Node.js code.”

They debug:

  • kernel queues
  • event loop behavior
  • network backpressure
  • system-wide flow

Because by the time your callback runs…

…the real story has already happened.

Understanding this journey gives you a superpower: you stop guessing — and start reasoning about the system end-to-end.


Next in the Series

Continue reading: Backpressure in Node.js — The Problem Most APIs Ignore Until They Crash