DEV Community

Cover image for Address already in use: quién tiene el puerto y por qué kill -9 va al final
Valen Torassa
Valen Torassa

Posted on Originally published at vtsecurity.com.ar

Address already in use: quién tiene el puerto y por qué kill -9 va al final

Cómo encontrar el proceso con ss, ver quién lo levantó y cortarlo sin kill -9. Y qué significa que esté escuchando en 0.0.0.0.

Pasa así: levantás tu app, la terminal te devuelve EADDRINUSE y no arranca. Buscás el error y la primera respuesta dice kill -9 $(lsof -t -i:3000). Lo corrés y anda.

Casi siempre. A veces el puerto vuelve a estar ocupado un segundo después. Otras, lo que mataste era la base de otro proyecto que tenías abierto. Y en el camino se pasa por alto algo que importa más que el error: ese proceso que no sabías que seguía corriendo también podía estar atendiendo a otras máquinas.

Quién tiene el puerto

El error solo dice el puerto. Para saber quién lo tiene está ss, que viene en cualquier distro actual:

$ node -e "require('http').createServer().listen(3000)"
Error: listen EADDRINUSE: address already in use :::3000
$ ss -ltnp 'sport = :3000'
State  Recv-Q Send-Q Local Address:Port Peer Address:PortProcess
LISTEN 0      5            0.0.0.0:3000      0.0.0.0:*    users:(("python3",pid=3365731,fd=3))
Enter fullscreen mode Exit fullscreen mode

-l muestra lo que está escuchando, -t TCP, -n números en vez de nombres y -p el proceso. La última columna es la respuesta: un python3 con PID 3365731. Si esa columna sale vacía, el proceso es de otro usuario y con sudo adelante aparece.

Con el PID, ps te dice qué es y hace cuánto corre:

$ ps -o pid,ppid,user,etime,cmd -p 3365731
    PID    PPID USER         ELAPSED CMD
3365731 3365724 valen          00:01 python3 -m http.server 3000
Enter fullscreen mode Exit fullscreen mode

Acá dice un segundo porque lo levanté para el ejemplo; uno olvidado suele decir días. python3 -m http.server es un clásico: lo usaste para pasar un archivo y quedó corriendo en otra terminal, sirviendo la carpeta entera donde lo levantaste, archivos ocultos incluidos.

En macOS ss no existe. lsof -i :3000 te da lo mismo, y en Linux también anda.

Qué quiere decir 0.0.0.0

Volvé a la columna Local Address. 0.0.0.0:3000 quiere decir que el proceso acepta conexiones por cualquier interfaz de la máquina: le puede hablar cualquiera que esté en tu red.

En el artículo original esto es una figura donde mandás pedidos y cambiás la dirección mientras llegan. Con 0.0.0.0, cualquiera en tu red llega al puerto y se baja el .env. Con 127.0.0.1, solo tu navegador:

tu máquina: python3 -m http.server sirve .env y app.js

escucha en 0.0.0.0:3000
  tu navegador             recibe la respuesta
  otra máquina de tu red   se lleva el .env (OPENAI_API_KEY=sk-…)
  internet                 lo corta el router

escucha en 127.0.0.1:3000
  tu navegador             recibe la respuesta
  otra máquina de tu red   conexión rechazada
  internet                 lo corta el router
Enter fullscreen mode Exit fullscreen mode

Red de ejemplo. El router corta lo que entra de internet mientras nadie le abra el puerto.

Lo probé con ese mismo servidor, levantado en una carpeta de prueba con un .env. Desde otra máquina de la red:

$ curl -s http://<ip>:3000/.env
OPENAI_API_KEY=sk-proj-demo-no-es-real
Enter fullscreen mode Exit fullscreen mode

La key es de mentira y <ip> reemplaza la de mi máquina. Levantado con --bind 127.0.0.1, el mismo curl devuelve Couldn't connect to server. Y si lo corrés en tu home, la carpeta que se sirve incluye .ssh.

El error de Node de más arriba también lo dice: :::3000. Si no le pasás una dirección, listen(3000) escucha en ::, que son todas.

Un firewall o el router pueden cortar la conexión antes. En una red que no es tuya, no contaría con eso.

kill manda una señal

kill no mata: le manda una señal al proceso. Si no le decís cuál, manda TERM, que le pide terminar y lo deja cerrar lo que tiene abierto. kill -9 manda KILL, que el programa no puede atender: el kernel lo saca en el acto.

En el artículo original hay una figura con un programa que atiende pedidos y cada tanto guarda en el disco. Le mandás una señal, lo levantás de nuevo y probás la otra. Esto es lo que pasa con cada una:

# app (PID 4321) atiende pedidos, cada tanto guarda en datos.db y tiene su lock en app.pid

$ kill 4321       # TERM
# deja de aceptar pedidos nuevos y termina los que tenía
# guarda todo en datos.db y borra app.pid
# lo que llega después rebota: no hay nadie escuchando

$ kill -9 4321    # KILL
# el kernel lo saca en el acto
# el pedido a medio atender se corta y los cambios que tenía en memoria se pierden
# app.pid queda, y sigue diciendo que ya está corriendo (PID 4321)
Enter fullscreen mode Exit fullscreen mode

Programa de ejemplo. No todos dejan un lock, pero cualquiera pierde lo que no alcanzó a escribir.

Señal Número Qué pide ¿La puede atender el proceso?
HUP 1 Varios servicios la usan para releer la configuración Sí
INT 2 Es lo que manda Ctrl+C Sí
TERM 15 Que termine: cerrar lo que tiene abierto y salir Sí
KILL 9 Nada. El kernel lo saca en el momento No

El orden es kill, esperar unos segundos, y kill -9 solo si sigue ahí.

Si vuelve solo, es un servicio

A veces matás el proceso y el puerto vuelve a estar ocupado al segundo. Lo reproduje con un servicio de systemd que se reinicia solo, y pasó exactamente eso.

En el artículo original hay una figura donde le mandás pedidos, lo matás y mirás qué pasa con los que llegan mientras no está:

# systemd --user vigila demo-web.service (Restart=always)
# python3 -m http.server, con PID 3366803, tiene el puerto :3000

$ kill -9 3366803
# el proceso murió: lo que llega rebota hasta que systemd lo levanta
# un segundo después hay otro PID, 3366835, y el puerto vuelve a contestar

$ systemctl --user stop demo-web
# systemctl stop le avisa a systemd: nadie lo relanza y todo lo que llega rebota
Enter fullscreen mode Exit fullscreen mode

Los dos PID son los de la prueba real.

El padre de ese proceso es systemd, así que matarlo no sirve. systemctl status acepta un PID y te dice de qué servicio es:

$ systemctl --user status 3366835 | head -4
● demo-web.service - [systemd-run] /usr/bin/python3 -m http.server 3000 --bind 127.0.0.1
     Loaded: loaded (/run/user/1000/systemd/transient/demo-web.service; transient)
  Transient: yes
     Active: active (running) since Fri 2026-09-25 11:21:28 -03; 1s ago
Enter fullscreen mode Exit fullscreen mode

Con eso sabés qué parar: systemctl --user stop demo-web. En un servidor lo normal es un servicio del sistema, y va sin --user. systemctl disable hace que no vuelva a arrancar con la máquina.

Si en la columna del proceso dice docker-proxy, el puerto lo publicó un contenedor. docker ps te dice cuál, y se para con docker stop.

El criterio

Antes de matar un proceso, fijate qué es y quién lo levantó: ss -ltnp y ps te lo dicen en dos líneas. Si vuelve solo, es un servicio y se para con systemctl stop.

Y mirá la dirección en la que escucha. Si dice 0.0.0.0 y no sabés por qué, arreglá eso antes que el error.


Practicalo en el lab: Del proceso al socket: una investigación con datos de juguete

Lo publiqué primero en vtsecurity.com.ar, donde las figuras son interactivas.

Top comments (1)

Collapse
 
_firelinks profile image
Mike Dabydeen •

Agregaría una excepción a la línea del firewall: si el puerto lo publica Docker, ufw no lo corta. Con docker run -p 3000:3000 el puerto queda en todas las interfaces, y Docker desvía ese tráfico en la tabla nat antes de que llegue a las cadenas INPUT donde ufw aplica sus reglas. La documentación de Docker lo dice tal cual en la sección "Docker and ufw". Así que en el caso de docker-proxy que mencionas, un ufw deny 3000 no protege el .env aunque el comando no dé error.

El arreglo es el mismo que hiciste con --bind, pero en la publicación: -p 127.0.0.1:3000:3000, o "127.0.0.1:3000:3000" en compose. docker ps lo muestra en la columna PORTS. Si ves 0.0.0.0:3000->3000/tcp, el contenedor está expuesto a la red aunque el firewall diga lo contrario.