Respuesta corta: para matar el proceso que ocupa un puerto, identifica primero su PID y después deténlo. En macOS y Linux: lsof -ti :3000 | xargs kill (y kill -9 solo si el proceso se resiste). En Linux con psmisc instalado, fuser -k 3000/tcp hace ambas cosas en un solo comando. En Windows: netstat -ano | findstr :3000 y luego taskkill /PID <pid> /F. Dentro de un contenedor Docker no hace falta un kill, sino docker stop.
El mensaje es siempre el mismo: Error: listen EADDRINUSE: address already in use :::3000 al arrancar next dev, o bind: address already in use en un kubectl port-forward. Un proceso sigue reteniendo el puerto: a menudo un servidor de desarrollo lanzado en una pestaña de terminal cerrada demasiado rápido, a veces un contenedor olvidado, a veces un túnel SSH. Estos son los comandos que usamos a diario, las diferencias entre sistemas y los tres casos en los que matar el proceso es una mala idea.

¿Cómo matar el proceso que ocupa un puerto?
En dos pasos: encontrar el PID que escucha en el puerto y después enviar una señal de parada a ese PID. El comando depende del sistema.
| Sistema | Identificar | Matar | |
|---|---|---|---|
| macOS | lsof -i :3000 | kill $(lsof -ti :3000) | |
| Linux (moderno) | ss -ltnp 'sport = :3000' | kill <pid> | |
Linux (psmisc) | fuser 3000/tcp | fuser -k 3000/tcp | |
| Windows (cmd) | `netstat -ano \ | findstr :3000` | taskkill /PID <pid> /F |
| Windows (PowerShell) | Get-NetTCPConnection -LocalPort 3000 | Stop-Process -Id <pid> | |
| Docker | docker ps --filter publish=3000 | docker stop <nombre> |
El detalle que importa: lsof -t solo devuelve los PID, lo que lo hace directamente componible con kill. En macOS, lsof -ti :3000 es la forma más corta que funciona sin sudo para tus propios procesos. lsof está documentado en la página de manual de las herramientas de Apple y viene de serie en macOS; en Linux ya no siempre está instalado, de ahí el interés de conocer ss, incluido en iproute2.
La versión Linux sin instalar nada
ss forma parte del paquete base de la mayoría de las distribuciones desde el abandono de net-tools:
# ¿Quién escucha en 3000? (-l listen, -t tcp, -n numérico, -p proceso)
sudo ss -ltnp 'sport = :3000'
# LISTEN 0 511 *:3000 *:* users:(("next-server",pid=48213,fd=23))
kill 48213
Sin sudo, la columna users: queda vacía para los procesos que no te pertenecen: ves el puerto ocupado pero no al culpable. Es la causa número uno del «mi comando no encuentra nada aunque el puerto esté ocupado».
El alias que conviene guardar
Ponemos sistemáticamente esta función en los dotfiles de los equipos de desarrollo, porque evita copiar y pegar un PID:
# ~/.zshrc o ~/.bashrc
killport() {
local pid
pid=$(lsof -ti ":$1" -sTCP:LISTEN)
if [ -z "$pid" ]; then
echo "Ningún proceso escuchando en el puerto $1"
return 1
fi
echo "Deteniendo el PID $pid en el puerto $1"
kill "$pid" || kill -9 "$pid"
}
El -sTCP:LISTEN es importante: sin él, lsof -ti :3000 también devuelve las conexiones salientes hacia el puerto 3000 de otra máquina. Matarías entonces tu navegador o tu cliente HTTP en lugar del servidor.
¿Por qué no hacer directamente kill -9?
Porque kill -9 envía SIGKILL, que el proceso no puede interceptar: sin cierre de conexiones, sin vaciado de búferes, sin limpieza de archivos temporales. En un servidor de desarrollo Next.js es inocuo. En otros tres casos, no.
- Una base de datos que escucha en 5432 o 3306: un SIGKILL fuerza una recuperación al reiniciar. PostgreSQL lo gestiona (es el equivalente de un corte de corriente), pero la parada limpia sigue siendo
pg_ctl stoposystemctl stop postgresql. La documentación de PostgreSQL sobre la parada del servidor detalla los modossmart,fasteimmediate; SIGKILL es peor queimmediate. - Un proceso con hijos: un
killsobre el padre puede dejar hijos huérfanos que siguen reteniendo el puerto (clásico connpm run dev, que lanza un subproceso). Mata el grupo:kill -- -$(ps -o pgid= <pid> | tr -d ' '). - Un contenedor: el PID visto desde el host es el del proceso dentro del contenedor. Matarlo deja a Docker en un estado incoherente;
docker stopenvía SIGTERM y después SIGKILL tras el plazo de gracia (10 segundos por defecto).
El orden correcto es, por tanto, siempre: SIGTERM (kill), esperar dos segundos y SIGKILL (kill -9) solo si el puerto sigue ocupado.
El puerto está libre pero el bind falla igualmente
Tres causas, todas frecuentes, y ninguna se resuelve con un kill.
1. Estado TIME_WAIT. Tras el cierre de un socket, el kernel mantiene la cuádrupla en TIME_WAIT durante unos 60 segundos (2×MSL en Linux). Un servidor que no activa la opción SO_REUSEADDR se negará a relanzarse durante ese plazo. Compruébalo con ss -tan state time-wait '( sport = :3000 )'. La única respuesta correcta es esperar, o corregir el servidor: no tocar net.ipv4.tcp_tw_reuse.
2. Puerto reservado por Windows. Hyper-V, WSL2 y el Windows NAT Service reservan rangos de puertos dinámicos al arrancar, y el puerto 3000 o 5000 puede caer dentro. En ese caso netstat no muestra ningún proceso. Lista las exclusiones:
netsh interface ipv4 show excludedportrange protocol=tcp
Si tu puerto está en un rango excluido, cambia de puerto o resérvalo explícitamente con netsh int ipv4 add excludedportrange. Es el síntoma típico del «ayer funcionaba» después de reiniciar Windows.
3. Permisos en los puertos < 1024. En Linux, hacer bind en 80 o 443 requiere CAP_NET_BIND_SERVICE o root. El error es EACCES, no EADDRINUSE: lee el código de error antes de buscar un proceso.

¿Y en Docker, Kubernetes o CI?
El reflejo del kill no se aplica: en estos entornos el puerto está retenido por un objeto gestionado por un orquestador, y es al orquestador al que hay que recurrir.
- Contenedor Docker que publica el puerto.
docker ps --filter publish=3000indica el contenedor;docker stop <nombre>lo detiene limpiamente. Sidocker psno muestra nada, busca contenedores detenidos pero con un puerto todavía mapeado por el proxy:docker ps -ay, como último recurso, reinicia el demonio. kubectl port-forwardfantasma. Unport-forwardlanzado en segundo plano sobrevive al cierre del terminal.lsof -ti :8080 -sTCP:LISTENdevuelve un procesokubectl: ese es el que hay que matar, no un pod. Las buenas prácticas de acceso a los servicios de la documentación de Kubernetes recuerdan queport-forwardes una herramienta de depuración, no un modo de exposición.- Puerto ya ocupado dentro de un pod. Dos contenedores de un mismo pod comparten el namespace de red: dos
containerPort: 3000en el mismo pod provocan unEADDRINUSEen el segundo. El diagnóstico se hace en los logs del contenedor, no en el host. - Runner de GitLab CI. En un runner compartido, un job anterior puede haber dejado un servicio a la escucha. En lugar de matar a ciegas, haz que tus jobs escuchen en un puerto dinámico (
PORT=0cuando el servidor lo admite) o usa los servicios de GitLab CI, que obtienen su propia red. Es el mismo razonamiento que seguimos en nuestro pipeline GitLab CI/CD para Next.js en Kubernetes: un job que depende del estado del runner no es reproducible.
¿Cómo evitar el problema en lugar de sufrirlo?
Haciendo que el puerto no genere conflictos por diseño. Cuatro hábitos que eliminan el 90 % de los casos:
- Un puerto por proyecto, escrito en el
.envdel repositorio y no en la cabeza de las personas que desarrollan: 3001 para la app A, 3002 para la app B. Next.js aceptanext dev -p 3001oPORT=3001. - Detener con Ctrl+C, nunca cerrando la pestaña. Cerrar la ventana del terminal no siempre envía SIGHUP al grupo de procesos, según el emulador y la shell; es el origen más banal de los servidores huérfanos.
docker compose downen lugar deCtrl+Cen uncompose up: el primero también retira la red y los mapeos de puertos.- Un script
predevque libere el puerto antes de arrancar, para los proyectos de equipo:
{
"scripts": {
"predev": "lsof -ti :3000 -sTCP:LISTEN | xargs -r kill || true",
"dev": "next dev"
}
}
El -r de xargs (GNU) evita llamar a kill sin argumento cuando el puerto está libre; en macOS, usa xargs sin -r junto con el || true.

El caso particular del puerto ocupado en producción
En producción, un puerto ocupado no es una molestia de puesto de desarrollo: es un despliegue bloqueado. El escenario clásico es un rolling update en el que el pod antiguo todavía no ha liberado su puerto cuando el nuevo arranca. En Kubernetes, con hostPort o hostNetwork: true, el conflicto está garantizado: el puerto se ocupa en el nodo, no en el pod. La respuesta no es matar un proceso, sino eliminar hostPort y pasar por un Service más un ingress o una Gateway, el mecanismo descrito en nuestro artículo sobre la Gateway API como sustituto de ingress-nginx.
En una VM de un solo servidor, el mismo conflicto aparece entre dos versiones lanzadas por systemd. El orden correcto es ExecStop y después ExecStart, con un TimeoutStopSec superior al plazo de cierre de conexiones de tu servidor HTTP (Node.js no cierra las keep-alive antes de server.close() más el final de las peticiones en curso).
Preguntas frecuentes
¿Cuál es el comando «kill port» más corto?
En macOS y Linux con lsof instalado: kill $(lsof -ti :3000 -sTCP:LISTEN). En Linux con psmisc: fuser -k 3000/tcp. En Windows: taskkill /PID <pid> /F después de leer el PID en netstat -ano | findstr :3000. No existe un comando killport nativo en estos sistemas; siempre es la composición de dos herramientas.
¿Por qué lsof -i :3000 no devuelve nada aunque el puerto esté ocupado?
Tres razones, por orden de frecuencia: no eres root y el proceso pertenece a otro usuario (relánzalo con sudo); el puerto está retenido por un contenedor cuyo mapeo pasa por el proxy de Docker (búscalo con docker ps); el puerto está en un rango reservado por Windows/WSL2 (netsh interface ipv4 show excludedportrange).
¿Hace falta reiniciar para liberar un puerto?
No, salvo en casos patológicos. Un puerto queda bloqueado o por un proceso (que se puede matar), o por un estado TIME_WAIT (que expira solo en aproximadamente un minuto), o por una reserva del sistema (modificable sin reiniciar en Windows con netsh). Reiniciar la máquina oculta la causa y esta vuelve en el siguiente arranque.
¿Cómo saber qué hace el proceso antes de matarlo?
ps -p <pid> -o pid,ppid,user,etime,command da la línea de comandos completa, el usuario y el tiempo de ejecución. Un etime de varios días en un puesto de desarrollo indica casi siempre un servidor huérfano; un proceso lanzado hace treinta segundos puede ser tu compañero haciendo pair programming en una sesión compartida.
En resumen
- Identificar antes de matar:
lsof -ti :<puerto> -sTCP:LISTEN(macOS/Linux),ss -ltnp(Linux),netstat -ano | findstr(Windows). - SIGTERM primero (
kill), SIGKILL (kill -9) solo como segundo recurso, y nunca el primero en una base de datos. - El filtro
-sTCP:LISTENevita matar una conexión saliente en lugar del servidor. - En Docker, Kubernetes o CI no se usa
kill:docker stop, eliminar elport-forwardo puerto dinámico en el job. - Puerto libre pero bind rechazado: piensa en
TIME_WAIT, en los rangos reservados de Windows/WSL2 o en los permisos de los puertos inferiores a 1024. - Prevención: un puerto por proyecto en el
.env, parada con Ctrl+C,docker compose down, scriptpredev.
Si estos conflictos de puertos también aparecen en tus entornos de preproducción o en tus despliegues, suele ser señal de una exposición de red construida a mano. Abordamos este tipo de arquitectura en nuestros proyectos de infraestructura y operación, e industrializamos los entornos de desarrollo desde el desarrollo de aplicaciones. Para hablarlo sobre un caso concreto, la página de contacto es el camino más corto.


