Node.js
The Node adapter runs your REST API on the built-in node:httpserver. It's the default target for containers, VMs, and any Node-based PaaS (Heroku, Railway, Render, Fly.io). Use it when you control the process, long-lived, observable, and easy to debug.
- clientHTTP requestTCP socket
- node:httpserve(app)timeouts · maxConnections
- coreapp.fetch(Request)routing · hooks · handler
- clientResponse
When to choose Node
- You deploy to a container, VM, or Node PaaS (no per-request billing).
- You need
node:*modules (filesystem, child processes, native addons). - You want the broadest npm package compatibility.
Scaffold
The fastest way to start is the node-basic template. It ships with TypeScript, pnpm workspaces, a /healthzroute, graceful shutdown, and Hey API codegen wired up.
Install
Requires Node.js 24 LTS or Node.js 26+. The adapter ships with @daloyjs/core; no extra dependency. Node.js 25 is not supported because it is already end-of-life.
Minimal server
What the adapter wires for you
requestTimeout,headersTimeout, andkeepAliveTimeoutset to safe production values. Both request timeouts derive fromconnectionTimeoutMs, and the adapter also lowers Node's connection-check interval to a fraction of that value so a slowloris (a client that stalls or trickles its request headers to hold a socket open) is reaped close to the deadline with a408, instead of surviving until Node's default 30 second sweep. SetconnectionTimeoutMs: 0to disable the timeouts entirely.- SIGTERM / SIGINT handlers that call
server.close()followed byserver.closeAllConnections()aftershutdownTimeoutMs: the pattern that became stable in Node 18.2 and is recommended on supported Node versions. - When
trustProxy: true, the adapter readsx-forwarded-protoandx-forwarded-hostwhen constructing the request URL. Leave it off unless TLS is terminated at a known proxy you control.
Behind a load balancer
Two rules to avoid the classic 502/504 race:
- Make your load balancer's idle timeout greater than DaloyJS's
requestTimeoutMs. - Make DaloyJS's
keepAliveTimeoutgreaterthan the load balancer's, the Node adapter does this for you.
Graceful degradation under overload
Steady-state throughput is only half the story. Once a Node process is pushed past saturation, the multi-second part of the tail latency no longer lives in your handler, it lives in the accept queue, where overflow connections sit waiting for the event loop to get to them. A connection sweep makes this visible: at high concurrency an unbounded server's p99.9 can cliff from tens of milliseconds into the multi-second range, even though median throughput still looks healthy.
The cheapest fix that actually works is connection-layer admission control: maxConnections forwards to Node's server.maxConnections, so once the cap is reached the server refuses additional sockets at accept time instead of queuing them into the event loop. Admitted traffic stays fast; overflow is rejected fast. It is off by default and sits off the request hot path, so it adds no per-request cost.
Pick the cap empirically: run a connection sweep against your real routes and set maxConnections at (or just below) the concurrency where p99/p99.9 latency stays in its healthy range. The right value is workload-specific, CPU-bound JSON validation saturates at a very different point than I/O-bound proxying.
Pair it with an upstream gateway
When the cap is hit, the overflow socket is refused at the TCP layer, the client sees a connection reset, not an HTTP response. In production you want a load balancer or API gateway in front that translates that refusal into a clean 503 Service Unavailable with a Retry-After header, so well-behaved clients back off and retry instead of hammering a saturated process.
Pair it with loadShedding
maxConnections and loadShedding() solve different layers of the same problem and compose well:
maxConnections(connection layer) caps how many sockets are ever accepted, keeping the event loop in its measured sweet spot.loadShedding()(application layer) sheds requests when an honest overload signal, event-loop delay (queue backlog) or in-flight concurrency, trips a threshold.
A note on the load-shedding signal: event-loop utilization is the wrong knob for an always-busy, CPU-bound server, which can sit near 100% utilization while perfectly healthy and would shed good traffic. Event-loop delay (how far behind the loop has fallen) is the honest overload signal. Likewise, requestTimeoutMs alone does not fix the cliff: it wraps handler execution, not the accept-queue wait where the multi-second tail actually lives.
Treat maxConnectionsas a resilience/latency lever, not a throughput lever, under overload it turns “everyone waits seconds” into “admitted traffic stays fast, overflow is refused fast.”
Dockerfile
Gotchas
- Don't put
process.exit()in a SIGTERM handler, letclose()drain. The adapter handles the hard kill after the timeout. - Set
hostname: "0.0.0.0"in containers; Node binds tolocalhostby default and that's invisible from outside the container.