NodeJS (libuv) kqueue(): Too many open files in system

Viewed 179

I'm trying to implement a simple HTTP server using node's net module. When I test the throughput using bombardier, node's HTTP module handles every connection without a problem, but net module fails instantly. My question is, how does node's http handles new connection or connection pooling? Example:

const http = require("http");

http
  .createServer((_, res) => {
    res.end();
  })
  .listen(4000);

const net = require("net");

net
  .createServer((socket) => {
    socket.end("HTTP/1.0 200 OK\r\n\r\n");
  })
  .listen(4000);

Then I run bombardier -c 126 -n 1000000 http://localhost:4000 and http runs wonderful but net throws:

(libuv) kqueue(): Too many open files in system
node:tty:94
    throw new ERR_TTY_INIT_FAILED(ctx);
    ^

SystemError [ERR_TTY_INIT_FAILED]: TTY initialization failed: uv_tty_init returned ENFILE (file table overflow)
    at new SystemError (node:internal/errors:251:5)
    at new NodeError (node:internal/errors:336:7)
    at new WriteStream (node:tty:94:11)
    at createWritableStdioStream (node:internal/bootstrap/switches/is_main_thread:47:16)
    at process.getStderr [as stderr] (node:internal/bootstrap/switches/is_main_thread:134:12)
    at console.get (node:internal/console/constructor:216:44)
    at console.value (node:internal/console/constructor:333:50)
    at console.warn (node:internal/console/constructor:368:61)
    at Server.<anonymous> (/Users/brielov/Work/Self/speedtest/net.js:8:37)
    at Server.emit (node:events:365:28) {
  code: 'ERR_TTY_INIT_FAILED',
  info: {
    errno: -23,
    code: 'ENFILE',
    me⏎

1 Answers

The http module has the concept of the Agent, which implements a connection pool. If you do not specify it, you get the global default agent (http.globalAgent).

In the net module, which is more low-level, you need to manage the sockets yourself. One way to avoid running out of sockets in your example, is to destroy the socket once the request is done:

const net = require("net");

net
  .createServer((socket) => {
    socket.end("HTTP/1.0 200 OK\r\n\r\n");
    socket.destroy();
  })
  .listen(4000);

Note that from a performance perspective, always calling destroy might not be optimal. Reusing connections and implementing a pool is a good idea. But for understanding why the original implementation quickly exhausts the resources, the missing destroy call should be the reason. Other libraries, which operate on a higher abstraction will implement that functionality under the hood.

Another way to deal with exceeding the socket limit of the system, is to limit the connection inside the net module by overriding server.maxConnections. Relying on that functionality alone is not recommended. It will eliminate the exception that you get, but the server will start rejecting all incoming requests once the limit is exceed. Thus, the effect is similar: the server is down.

Related