← Volver al blog

Guía

¿Qué es EADDRINUSE? El error, la causa, y el caso donde no hay nada escuchando

14 de septiembre de 20264 min de lectura

Error: listen EADDRINUSE: address already in use :::3000

Casi todo desarrollador ha leído esa línea. Aparece en Node, pero no es un error de Node. Es una llamada al sistema que falla, y el nombre es el mismo en todas partes.

Leyendo el nombre

EADDRINUSE se parte en tres. La E lo marca como constante de error, la convención de Unix. ADDR es dirección. IN USE es exactamente lo que dice.

Lo devuelve bind(), la llamada al sistema que hace un servidor cuando reclama una dirección y un puerto. Tu programa le pidió al kernel el puerto 3000, y el kernel dijo que no, porque la regla es que solo un socket puede escuchar en una dirección y puerto dados a la vez.

Por eso el texto del mensaje cambia según el lenguaje mientras el código no. Python lanza OSError: [Errno 48] Address already in use, Go devuelve bind: address already in use, Node imprime EADDRINUSE. El mismo rechazo por debajo.

Fíjate en el :::3000 del mensaje de Node. Esos tres dos puntos son la dirección comodín de IPv6, o sea que el servidor estaba intentando ocupar todas las interfaces en IPv6.

La causa común

Nueve de cada diez veces es una corrida anterior del mismo servidor que no salió limpiamente. Se cerró la terminal, el proceso quedó suspendido y nunca volvió, un crash dejó un huérfano, o un observador de archivos reinició algo antes de que el anterior soltara el puerto.

El arreglo es encontrarlo y terminarlo:

lsof -i :3000

Y después, cuando ya sabes qué es:

lsof -ti :3000 | xargs kill

-t imprime solo los IDs de proceso, y xargs se los pasa todos a un solo kill. Ese detalle importa con servidores que corren varios workers, porque cada worker ocupa el mismo puerto y quieres que se vayan todos.

La otra causa: no hay nada escuchando

Esta es la que hace pensar que algo está profundamente roto. Te sale EADDRINUSE, corres lsof -i :3000, y no imprime absolutamente nada.

El puerto está en TIME_WAIT.

Cuando una conexión TCP se cierra, el lado que cerró primero mantiene el puerto reservado un rato, típicamente unos 30 segundos en macOS. La razón es cuidadosa, no arbitraria: podrían quedar paquetes sueltos de la conexión recién terminada todavía en tránsito, y reutilizar el puerto de inmediato arriesga entregárselos a una conexión nueva que no tiene nada que ver con ellos.

Así que el socket ya no está, ningún proceso lo posee, y el puerto igual no está disponible. lsof -i :3000 no muestra nada porque no hay proceso. Para ver el estado:

netstat -an | grep 3000

Si ves TIME_WAIT, ahí está. El arreglo honesto es esperar medio minuto.

Si prefieres no esperar, para eso existe SO_REUSEADDR. La mayoría de los frameworks ya lo activan, y por eso rara vez te topas con esto en el día a día y por eso se siente tan raro cuando pasa.

Por qué pasa más con algunas herramientas

Algunos flujos de trabajo lo producen más seguido que otros.

El modo watch y el hot reload reinician el servidor apenas cambia un archivo. Si el reinicio es más rápido que la liberación del puerto por parte del proceso viejo, corres una carrera contra ti mismo.

Docker publica los puertos de contenedor a través de un proceso docker-proxy. Un contenedor que no se detuvo limpiamente puede dejar el puerto publicado tomado, y el nombre del proceso no te dice nada sobre cuál contenedor era.

Los servidores multi worker como gunicorn u Odoo ocupan un puerto entre varios procesos. Matar un worker no libera nada, porque los demás lo siguen teniendo. Esto es exactamente por qué importa la forma con xargs de más arriba.

Prevenirlo en vez de arreglarlo

Detén tu servidor de desarrollo con Ctrl+C en vez de cerrar la ventana de la terminal. Ctrl+C manda SIGTERM y le permite al proceso soltar el puerto; cerrar la ventana puede dejarlo huérfano.

Si un proyecto en particular te choca siempre, dale un puerto que nadie más quiera. Hay harto espacio entre 1024 y 49151, y escribimos sobre cómo funcionan esos rangos.

Bosun vive en la barra de menú y muestra qué está ocupando cada puerto antes de que te llegue el error, con los contenedores de Docker nombrados como corresponde en vez de aparecer como docker-proxy. macOS 14 o posterior, prueba de 14 días, sin cuenta.

Ve esto en vez de escribirlo

Bosun vive en tu barra de menú y muestra cada puerto abierto en tu Mac, en vivo, mapeado al proceso detrás. Un clic para matarlo, SIGTERM primero. Útil la primera vez que pasa esto. Realmente útil la quinta vez que pasa en una misma tarde.