Instantly share code, notes, and snippets.

What would you like to do?
do: exes
SRC := $(wildcard *.c)
EXE := $(SRC:.c=)
exes: $(EXE)
%: %.c
gcc -Wall -Werror -o $@ $^


The event loop from the inside out

Sam Roberts

github: @sam-github


twitter: @octetcloud

Goal, to be able to answer these questions:

  • What is the event loop? (Hint: its not an EventEmitter)
  • When is node multi-threaded?
  • Why is Node.js said to "scale well"?

A primer in Unix system programming

Warning: Pseudo "C" code lies ahead!

Network connections use "sockets", named after the system call used:

  int s = socket();

Sockets are referred to (confusingly) as "file descriptors", these are not necessarily references to the file system. Sorry.

File descriptors are O/S "object orientation", they point to objects in the kernel with a virtual "interface" (read/write/close/etc.).

Scale problem: thread-per-connection

    int server = socket();
    bind(server, 80)
    while(int connection = accept(server)) {
      pthread_create(echo, connection)

    void echo(int connection) {
      char buf[4096];
      while(int size = read(connection, buffer, sizeof buf)) {
        write(connection, buffer, size);

Scale solution: kqueue, epoll, overlapped I/O

    int server = ... // like before

    int eventfd = epoll_create1(0);
    struct epoll_event events[10];
    struct epoll_event ev = { .events = EPOLLIN, .data.fd = server };
    epoll_ctl(epollfd, EPOLL_CTL_ADD, server, &ev);

    // This *is* the "event loop", every pass is a "tick"
    while((int max = epoll_wait(eventfd, events, 10, -1))) {
      for(n = 0; n < max; n++) {
        if (events[n].data.fd.fd == server) {
          // Server socket has connection!
          int connection = accept(server);
 = EPOLLIN; = connection;
          epoll_ctl(eventfd, EPOLL_CTL_ADD, connection, &ev);
        } else {
          // Connection socket has data!
          char buf[4096];
          int size = read(connection, buffer, sizeof buf);
          write(connection, buffer, size);

What is the node event loop?

A semi-infinite loop, polling and blocking on the O/S until some in a set of file descriptors are ready.

When does node exit?

It exits when it no longer has an events to epoll_wait() for, so will never have any more events to process. At that point the epoll loop must complete.

Note: .unref() marks handles that are being waited on in the loop as "not counting" towards keeping node alive.

Can we poll for all Node.js events?

Yes and no.

  • "file" descriptors: yes, but not actual disk files (sorry)
  • time: yes
  • anything else... indirectly

Pollable: sockets (net/dgram/http/tls/https)

Classic, well supported.

Pollable: time (timeouts and intervals)

    poll(..., int timeout)
    kqueue(..., struct timespec* timeout)
    epoll_wait(..., int timeout, ...)

timeout resolution is milliseconds, timespec is nanoseconds, but rounded up to system clock granularity.

Only one timeout at a time, but Node.js keeps all timeouts sorted, and sets the timeout value to the next/earliest timeout.

Not pollable: file system

fs.* use the uv thread pool (unless they are sync).

The blocking call is made by a thread, and when it completes, readiness is signalled back to epoll loop using either an eventfd or a self-pipe.

Aside: self-pipe

A pipe, where one end is written to by a thread or signal handler, and the other end is polled in the epoll loop.

Traditional way to "wake up" a polling loop when the event to wait for is not directly representable as a file descriptor.

Sometimes pollable: dns

  • dns.lookup() calls getaddrinfo(), a function in the system resolver library that makes blocking socket calls and cannot be integrated into a polling loop.
  • dns.<everything else> uses non-blocking I/O, and integrates with the epoll loop

Docs bend over backwards to explain this, but once you know how the event loop works, and how blocking library calls must be shunted off to the thead pool, this will always makes sense.

Important notes about the UV thread pool

It is shared by:

  • fs,
  • dns,
  • http.request() (with a name, dns.lookup() is used to resolve), and
  • any C++ addons that use it.

Default number of threads is 4, significantly parallel users of the above should increase the size.


  • Resolve DNS names yourself, directly, using the direct APIs to avoid dns.lookup().
  • Increase the thread pool size with UV_THREADPOOL_SIZE.

Pollable: signals

The ultimate async... uses the self-pipe pattern to communicate with epoll loop.

Note that attaching callbacks for signals doesn't "ref" the event loop, which is consistent with their usage as a "probably won't happen" IPC mechanism.

Pollable: child processes

  • Unix signals child process termination with SIGCHLD
  • Pipes between the parent and child are pollable.

Sometimes pollable: C++ addons

Addons should use the UV thread pool, but can do anything, including making blocking calls which will block the loop (perhaps unintentionally).


  • Review their code
  • Track loop metrics

You should now be able to describe:

  • What is the event loop
  • When is node multi-threaded
  • Why it "scales well"


This talk, including compilable version of pseudo "C" for playing with:

Bert Belder's talk about the Node.js event loop from a higher level, the "outside in":

View Raw

(Sorry about that, but we can’t show files that are this big right now.)

View Raw

(Sorry about that, but we can’t show files that are this big right now.)

View Raw

(Sorry about that, but we can’t show files that are this big right now.)

View Raw

(Sorry about that, but we can’t show files that are this big right now.)

Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View Raw

(Sorry about that, but we can’t show files that are this big right now.)

View Raw

(Sorry about that, but we can’t show files that are this big right now.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment