Quick Summary: Debugging a Node.js EADDRNOTAVAIL on localhost? This guide cuts through the noise, exposing an obscure Linux kernel interaction with IPv6 disabled...Alright, listen up. You've got a Node.js app, it works everywhere, but then you deploy it to some crusty VM or a specialized container, and suddenly,
EADDRNOTAVAIL. On localhost. You ping localhost, it works. You netstat, nothing's listening. You're losing your mind. I've been there. This isn't your usual "port already in use" garbage. This is deeper. This is a phantom.The Symptoms
Your Node.js application, usually a web server or a microservice attempting to bind to
127.0.0.1 or just localhost, throws this beauty:Error: listen EADDRNOTAVAIL: address not available 127.0.0.1:3000
at Server.setupListenHandle [as _setupListenHandle] (node:net:1882:21)
at Server.listen (node:net:1946:10)
at Object.<anonymous> (your-app.js:10:8)
at Module._compile (node:internal/modules/cjs/loader:1256:14)
at Module._extensions..js (node:internal/modules/cjs/loader:1310:10)
at Module.load (node:internal/modules/cjs/loader:1119:32)
at Module._load (node:internal/modules/cjs/loader:960:12)
at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:81:12)
at node:internal/main/run_main_module:23:47Notice it explicitly says 127.0.0.1. You're staring at it, thinking, "But that is available!" You try different ports. Same deal. You try 0.0.0.0. It works. You try binding to a specific external IP. It works. Only 127.0.0.1 or localhost fails.Initial Checks (And Why They Fail)
You've already run these. They all tell you everything is fine, which just adds to the rage.
ping localhost: Replies.127.0.0.1. Ok, great.ping 127.0.0.1: Replies. Wonderful.netstat -tulnp | grep 3000(or your port): Nothing. No zombie process, no rogue listener.ip a: Showslointerface up,127.0.0.1assigned.- Checked
sysctl net.ipv4.ip_local_port_rangeandnet.ipv4.ip_unprivileged_port_start? All default.
The Environment Where This Triggers
This specific nightmare manifests under a precise set of conditions. If you're on these, pay attention.
| Operating System | Kernel Version Range | Node.js Version | Additional Configuration |
|---|---|---|---|
| Debian/Ubuntu | 4.x to 5.4 (e.g., Ubuntu 18.04 LTS, Debian 9/10) | 14.x to 18.x (LTS releases) | IPv6 completely disabled via kernel parameters (ipv6.disable=1) |
| CentOS/RHEL | 3.x to 4.x (e.g., CentOS 7, RHEL 7) | 12.x to 16.x | IPv6 completely disabled via kernel parameters |
Deep Dive – Tracing the Failure
When Node.js's
net module goes to bind a server, especially when you specify localhost or omit the address entirely (allowing Node to implicitly resolve localhost), it does some interesting things. On systems with disabled IPv6, but where the system's hosts file still contains ::1 localhost, Node's resolver might still attempt to bind to ::1 before 127.0.0.1 or fail to properly handle the EADDRNOTAVAIL when the initial IPv6 bind fails.We're dealing with a system where IPv6 is brutally ripped out at the kernel level. Check your
/etc/default/grub or /etc/sysctl.conf for ipv6.disable=1. This isn't just a firewall rule; this is a full-on lobotomy.The Root Cause
This is a nasty interaction between a Node.js runtime's network stack assumptions, glibc's resolver behavior, and a kernel that has explicitly had IPv6 disabled. Even thoughlocalhost primarily resolves to 127.0.0.1 on IPv4-only systems, and most getaddrinfo calls would eventually provide 127.0.0.1, Node.js's internal uv_getaddrinfo (part of libuv) can, under specific kernel circumstances, attempt to bind to ::1 first or fail to correctly fall back to IPv4 when an explicit localhost or no address is provided, and IPv6 is completely unavailable at the syscall level. The kernel simply rejects any attempt to open an IPv6 socket, leading directly to EADDRNOTAVAIL even for the concept of ::1 or localhost when the binding attempt first probes IPv6. It's a race condition of sorts, where the OS outright refuses the family before Node can intelligently pivot. This is a low-level dance, a bit like the intricacies we sometimes see in high-performance networking setups, where even microsecond mayhem can derail operations. For a general overview of Node.js capabilities versus alternatives, you might find our Deno vs. Node.js article insightful.The Fix: Explicitly Force IPv4
The solution is annoyingly simple, but not obvious if you don't trace the syscalls or understand the kernel's stubbornness. You need to explicitly tell Node.js (and by extension, the underlying
net module) to only use IPv4 when resolving hostnames or binding.There are two primary ways to do this, depending on your application:
1. Code-level fix (if you can modify the app):
When creating your server, pass
0.0.0.0 or 127.0.0.1 explicitly, and crucial for this issue, set the ipv6Only option to false and potentially the family option to 4.const http = require('http');
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Hello Node.js!\n');
});
// The critical fix: Explicitly bind to IPv4
// Using 0.0.0.0 is often better in containers/VMs,
// but 127.0.0.1 works if you only need loopback.
server.listen(3000, '127.0.0.1', () => {
console.log('Server running on http://127.0.0.1:3000/');
});
// Alternatively, if using net.createServer directly or for more control:
// server.listen({
// port: 3000,
// host: '127.0.0.1', // or '0.0.0.0'
// family: 4, // Explicitly IPv4
// ipv6Only: false // Ensure it doesn't try to default to IPv6
// }, () => {
// console.log('Server running on http://127.0.0.1:3000/');
// });
2. Environment Variable Override (if you can't modify the app code easily):
This is the sledgehammer. It forces the underlying
getaddrinfo calls to prefer IPv4 globally for Node.js.NODE_OPTIONS="--dns-result-order=ipv4first" node your-app.jsOr, even better, set it globally in your environment or Dockerfile:
export NODE_OPTIONS="--dns-result-order=ipv4first"
node your-app.jsThis tells Node.js to always prioritize IPv4 addresses when resolving hostnames. For cases where you use
localhost without specifying 127.0.0.1, this helps ensure that 127.0.0.1 is the first (and likely only) address Node.js tries to bind to.Verification
After applying the fix, restart your application. The
EADDRNOTAVAIL error should vanish, and your application should bind successfully. Use netstat -tulnp | grep 3000 again, and you should see your Node.js process happily listening on 127.0.0.1:3000.Conclusion
This specific
EADDRNOTAVAIL isn't a simple config mistake; it's a deep-seated incompatibility between a legacy kernel configuration (IPv6 disabled) and modern runtime assumptions. It highlights why understanding the full stack, from kernel to application, is crucial for SREs. Don't let these phantom errors waste your precious time. Force IPv4 and move on.
Comments
Post a Comment