EADDRINUSE: address already in use — resolver no Node
Como descobrir quem está na porta 3000, matar o processo certo e por que o Express 5 diz que o servidor subiu quando ele não subiu.
EADDRINUSE, abreviação de “address already in use”, significa que o sistema
operacional recusou o listen porque outro socket já ocupa a combinação de
endereço e porta solicitada. O Node emite o erro; sem um listener para tratá-lo,
o processo termina.
Pense num estacionamento com vagas numeradas. A porta é o número da vaga, o
endereço indica em qual área ela fica e o processo é o carro. Dois carros não
podem reservar a mesma vaga na mesma área ao mesmo tempo. No sistema operacional,
essa reserva acontece no bind: você precisa identificar o PID que mantém o
socket, encerrar o processo correto ou escolher outra porta.
Todos os exemplos deste artigo são a API de agenda de uma clínica veterinária, a Pata Feliz. Uma rota, duas consultas, e a porta 3000 — a mesma porta que provavelmente está travando na sua máquina agora.
O erro por inteiro, campo por campo
Este é o servidor, escrito com o módulo node:http, sem framework nenhum:
import { createServer } from 'node:http';
const consultas = [
{ id: 1, pet: 'Frida', tutor: 'Ana', horario: '09:00' },
{ id: 2, pet: 'Tobias', tutor: 'Bruno', horario: '10:30' },
];
const servidor = createServer((req, res) => {
res.setHeader('content-type', 'application/json');
res.end(JSON.stringify(consultas));
});
servidor.listen(3000, () => {
console.log('Agenda da Pata Feliz em http://localhost:3000');
});Rodando com node agenda.mjs numa máquina em que a 3000 já estava ocupada, a
saída é esta — copiada inteira, porque a metade que as pessoas ignoram é
justamente a que resolve o problema:
Error: listen EADDRINUSE: address already in use :::3000 at Server.setupListenHandle [as _listen2] (node:net:2008:16) at listenInCluster (node:net:2065:12) at Server.listen (node:net:2170:7) at file:///private/tmp/pata-feliz/agenda.mjs:13:10 at ModuleJob.run (node:internal/modules/esm/module_job:439:25) at async node:internal/modules/esm/loader:633:26 at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:101:5) Emitted ‘error’ event on Server instance at: at emitErrorNT (node:net:2044:8) at process.processTicksAndRejections (node:internal/process/task_queues:90:21) { code: ‘EADDRINUSE’, errno: -48, syscall: ‘listen’, address: ‘::’, port: 3000 }
Node.js v24.16.0
O objeto no final da mensagem é o mais útil de tudo:
| campo | valor aqui | o que ele diz |
|---|---|---|
code |
EADDRINUSE |
o nome do erro do sistema operacional, não do Node |
errno |
-48 |
o número desse erro neste sistema; no Linux é -98 |
syscall |
listen |
a chamada de sistema que falhou |
address |
:: |
o endereço que você pediu — :: é “todas as interfaces”, em IPv6 |
port |
3000 |
a porta pedida |
O errno muda de sistema para sistema, e é por isso que você nunca compara
errno, sempre code. Dá para conferir o número da sua máquina sem sair do
Node:
import { constants } from 'node:os';
console.log('EADDRINUSE aqui =', constants.errno.EADDRINUSE);O mesmo servidor, rodado dentro de um container Linux (Alpine, Node 24.19.0),
devolve errno: -98. O code continua EADDRINUSE nos dois.
O :::3000 da primeira linha assusta, mas são só dois pontos-duplos do IPv6 e
mais um separador: endereço ::, porta 3000. Guarde esse campo address —
ele volta a importar mais para a frente.
Quem está segurando a porta agora
No macOS e no Linux, o comando que responde isso em um segundo é o lsof:
lsof -nP -iTCP:3000 -sTCP:LISTENO -n evita resolver DNS, o -P mostra o número da porta em vez do nome do
serviço, e o -sTCP:LISTEN filtra só quem está ouvindo. Sem esse filtro você vê
também as conexões abertas, e a lista fica confusa.
Repare no que apareceu: não é um node. A coluna COMMAND vem truncada em
nove caracteres, então com.docke ainda não conta a história toda. O ps
completa:
ps -o pid,ppid,command -p 9552No Linux o equivalente é o ss. Esta saída veio de dentro de um container
Alpine, logo depois de reproduzir o mesmo EADDRINUSE lá dentro:
ss -ltnpDetalhe que confunde muita gente: o ss mostra MainThread, e não node. Ele
imprime o nome da thread, e a thread principal do Node se chama assim. O
pid=16 é o que interessa.
No Windows, os dois comandos equivalentes são estes — o primeiro devolve o PID na última coluna, o segundo diz de quem é o PID:
netstat -ano | findstr :3000
tasklist /FI "PID eq 12345"O container que ficou publicando a 3000
Voltando ao lsof de antes: quem estava na 3000 era o Docker. Falta descobrir
qual container. O filtro publish faz isso sem você precisar ler a lista
inteira:
docker ps --filter publish=3000 --format "table {{.Names}}\t{{.Image}}\t{{.Ports}}"Na máquina em que este artigo foi escrito, o culpado era um container de outro projeto, subido semanas antes e nunca derrubado. Esse é o caso mais frustrante do EADDRINUSE: o processo não tem nada a ver com o código que você está tentando rodar.
Para mostrar o ciclo inteiro sem mexer no container alheio, subi um nginx meu
na porta 3005:
docker run -d --name pata-feliz-web -p 3005:80 nginx:alpine
lsof -nP -iTCP:3005 -sTCP:LISTENMesmo PID do Docker de novo, porque é sempre o Docker que abre a porta no host.
E aqui mora a confusão do -p 3005:80: o número antes dos dois-pontos é a
porta na sua máquina, e é essa que pode dar EADDRINUSE. O 80 depois dos
dois-pontos é a porta dentro do container, que não conflita com nada seu.
docker ps --filter publish=3005 --format "table {{.Names}}\t{{.Image}}\t{{.Ports}}"
docker stop pata-feliz-web
lsof -ti tcp:3005 || echo "(sem PID: porta livre)"Depois do docker stop, o node sobe na 3005 sem reclamar. Se o container
precisa continuar no ar, a correção é o contrário: mude a porta publicada para
-p 3006:80 e deixe a 3005 para o seu servidor.
Matar o processo certo — e o preço do kill -9
Quando o dono da porta é mesmo um node seu, o caminho curto é este:
lsof -ti tcp:3003
lsof -ti tcp:3003 | xargs killA primeira linha só mostra o PID; a segunda mata e não imprime nada. O -t faz
o lsof devolver o número puro, sem cabeçalho, que é exatamente o que o
xargs kill precisa. Sem -9, o kill envia SIGTERM — um pedido educado
para o processo se encerrar. E processo bem escrito aproveita esse pedido:
import { servidor } from './servidor.js';
servidor.listen(3003, () => {
console.log(`Agenda no ar na porta 3003 (pid ${process.pid})`);
});
process.on('SIGTERM', () => {
console.log('SIGTERM recebido: parando de aceitar novas requisições...');
servidor.close(() => {
console.log('Servidor fechado. Porta 3003 liberada.');
process.exit(0);
});
});Com kill <pid>, o log mostra o encerramento inteiro:
Com kill -9 <pid> no mesmo servidor, o log para na primeira linha:
O SIGKILL não pode ser interceptado: o processo simplesmente deixa de existir.
Nada de fechar conexão de banco, gravar o que estava em memória ou terminar a
requisição que estava no meio. Em desenvolvimento isso raramente machuca, mas
kill -9 é o segundo comando que você tenta, nunca o primeiro.
O import inocente que sobe um segundo servidor
Agora o caso que não é culpa de processo esquecido nenhum. A clínica tem um script que conta as consultas cadastradas, e ele importa o arquivo do servidor só para reaproveitar o array:
// servidor.js — com um listen no topo do módulo
import { createServer } from 'node:http';
export const consultas = [
{ id: 1, pet: 'Frida', tutor: 'Ana', horario: '09:00' },
{ id: 2, pet: 'Tobias', tutor: 'Bruno', horario: '10:30' },
];
export const servidor = createServer((req, res) => {
res.setHeader('content-type', 'application/json');
res.end(JSON.stringify(consultas));
});
servidor.listen(3003, () => console.log('Agenda no ar na porta 3003'));// seed.js — só quer contar, não quer servir nada
import { consultas } from './servidor.js';
console.log(`A Pata Feliz tem ${consultas.length} consultas cadastradas.`);Com o servidor de desenvolvimento rodando em outro terminal, node seed.js
imprime o que devia — e morre logo depois:
Error: listen EADDRINUSE: address already in use :::3003 at Server.setupListenHandle [as _listen2] (node:net:2008:16) at listenInCluster (node:net:2065:12) at Server.listen (node:net:2170:7) at file:///private/tmp/pata-feliz/servidor.js:13:10 at ModuleJob.run (node:internal/modules/esm/module_job:439:25) at async node:internal/modules/esm/loader:633:26 at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:101:5) Emitted ‘error’ event on Server instance at: at emitErrorNT (node:net:2044:8) at process.processTicksAndRejections (node:internal/process/task_queues:90:21) { code: ‘EADDRINUSE’, errno: -48, syscall: ‘listen’, address: ‘::’, port: 3003 }
Node.js v24.16.0
Leia a quarta linha do rastro: o erro aponta para servidor.js:13, e não para
seed.js. Importar um módulo executa o módulo inteiro, inclusive o listen
que estava lá no topo. O seed.js nunca pediu porta nenhuma; ele só importou
quem pedia.
A correção é separar o que o módulo é do que o programa faz: o
servidor.js só cria e exporta, e quem chama listen é o ponto de entrada.
// servidor.js — agora só monta e exporta
import { createServer } from 'node:http';
export const consultas = [
{ id: 1, pet: 'Frida', tutor: 'Ana', horario: '09:00' },
{ id: 2, pet: 'Tobias', tutor: 'Bruno', horario: '10:30' },
];
export const servidor = createServer((req, res) => {
res.setHeader('content-type', 'application/json');
res.end(JSON.stringify(consultas));
});// index.js — o único lugar do projeto que chama listen
import { servidor } from './servidor.js';
servidor.listen(3003, () => console.log('Agenda no ar na porta 3003'));Com essa separação, o mesmo node seed.js roda em paz com o servidor de
desenvolvimento no ar:
Vale a regra geral: listen só no arquivo de entrada. É a mesma separação
que deixa a API testável, como mostra a lição de
testar rota de API com node:test e supertest.
Express 5 diz que subiu, e não subiu
Este é o comportamento que mais faz gente perder uma tarde, e ele não aparece
em nenhum console.log acusador. Com a porta 3003 ocupada:
import express from 'express';
const app = express();
app.get('/consultas', (req, res) => {
res.json([{ id: 1, pet: 'Frida', tutor: 'Ana', horario: '09:00' }]);
});
app.listen(3003, (erro) => {
console.log('callback do listen chamado; erro =', erro?.code ?? 'nenhum');
console.log('Agenda (Express) na porta 3003');
});O processo terminou com código de saída 0. Nenhum rastro de erro, nenhuma
exceção, e um log dizendo que a agenda está na porta 3003 — quando ela não está.
Se você bater no localhost:3003, quem responde é o outro servidor.
O motivo está no código do próprio Express 5 (testei na versão 5.2.1): quando
você passa uma função como último argumento de app.listen, ele registra a
mesma função como tratador de erro do servidor:
app.listen = function listen() {
var server = http.createServer(this)
var args = slice.call(arguments)
if (typeof args[args.length - 1] === 'function') {
var done = args[args.length - 1] = once(args[args.length - 1])
server.once('error', done)
}
return server.listen.apply(server, args)
}Duas consequências saem daí. Primeira: como existe um listener de error, o
Node não lança mais a exceção — o processo apenas acaba. Segunda: o seu callback
recebe o erro no primeiro parâmetro, que quase ninguém declara. O
console.log('servidor no ar') roda do mesmo jeito.
A versão que não mente separa as duas coisas:
import express from 'express';
const app = express();
app.get('/consultas', (req, res) => {
res.json([{ id: 1, pet: 'Frida', tutor: 'Ana', horario: '09:00' }]);
});
const servidor = app.listen(3003);
servidor.on('listening', () => console.log('Agenda (Express) na porta 3003'));
servidor.on('error', (erro) => {
if (erro.code !== 'EADDRINUSE') throw erro;
console.error('A porta 3003 já está ocupada. Nada foi publicado.');
process.exit(1);
});Agora o processo sai com código 1, que é o que o seu terminal, o seu npm run dev e o seu deploy precisam ver. Se você ainda está montando a primeira rota,
a lição de Express do zero mostra o
esqueleto completo do arquivo.
Matei o processo e continua ocupada? Olhe o endereço
Aqui entra o campo address que aparecia lá no começo. A reserva do sistema
operacional não é da porta sozinha: é do par endereço + porta. Dois
servidores podem conviver na mesma porta se pedirem endereços diferentes:
import { createServer } from 'node:http';
const local = createServer((q, s) => s.end('sou o A, em 127.0.0.1'));
const publico = createServer((q, s) => s.end('sou o B, em todas as interfaces'));
publico.on('error', (e) => console.log('B falhou:', e.code, e.address, e.port));
local.listen(3003, '127.0.0.1', () => {
console.log('A ouvindo em', JSON.stringify(local.address()));
publico.listen(3003, async () => {
console.log('B ouvindo em', JSON.stringify(publico.address()));
const r = await fetch('http://127.0.0.1:3003');
console.log('quem responde em 127.0.0.1:3003 ->', await r.text());
local.close();
publico.close();
});
});Os dois subiram. Ninguém deu EADDRINUSE. E quem atende em
http://127.0.0.1:3003 é o primeiro, o mais específico — o segundo fica com o
resto das interfaces.
É esse comportamento que produz o sintoma mais desnorteante da família: você
altera o código, reinicia, o log diz que subiu, e a resposta continua sendo a
antiga. Não é cache do navegador. É outro processo, ainda vivo, ouvindo num
endereço mais específico que o seu. Por isso o lsof sem filtro de endereço é
o comando certo: ele lista todos os sockets naquela porta, não só o seu.
Um servidor que avisa em vez de despencar
Nada disso precisa virar um rastro de pilha de dez linhas na cara de quem está
começando no projeto. Um on('error') transforma o acidente em instrução:
import { servidor } from './servidor.js';
const PORTA = Number(process.env.PORT) || 3003;
servidor.on('error', (erro) => {
if (erro.code !== 'EADDRINUSE') throw erro;
console.error(`A porta ${PORTA} já está ocupada por outro processo.`);
console.error(`Descubra quem é: lsof -ti tcp:${PORTA}`);
console.error(`Ou suba em outra: PORT=${PORTA + 1} node index-guardiao.js`);
process.exit(1);
});
servidor.listen(PORTA, () => console.log(`Agenda no ar na porta ${PORTA}`));Repare em três decisões dentro dessas poucas linhas. O if (erro.code !== 'EADDRINUSE') throw erro garante que outros erros de rede continuem
aparecendo inteiros, em vez de serem engolidos por um tratador genérico. O
process.exit(1) avisa o sistema que a coisa falhou. E a porta vem de
process.env.PORT, o que deixa qualquer pessoa trocar de porta sem editar
código — o mesmo mecanismo explicado em
variáveis de ambiente no Node, e a
mesma exigência de todo serviço em nuvem, como aparece em
deploy de API Node.
Porta 0: a saída para a suíte de testes
Existe um caso em que a porta fixa é o problema, não a solução: os testes. Se
cada arquivo de teste sobe a API na 3000, o segundo arquivo quebra — e quebra
também quando alguém está com o npm run dev aberto. A resposta é pedir a
porta zero, que significa “sistema, escolha uma livre para mim”:
import { servidor } from './servidor.js';
servidor.listen(0, async () => {
const { port } = servidor.address();
console.log(`Servidor de teste subiu na porta ${port}`);
const resposta = await fetch(`http://localhost:${port}`);
const dados = await resposta.json();
console.log(`Recebi ${dados.length} consultas — status ${resposta.status}`);
servidor.close();
});Três execuções seguidas, sem tocar em nada entre elas:
A porta muda a cada execução, e servidor.address().port devolve a que foi
sorteada — por isso o listen(0) só serve com o endereço lido depois que o
servidor subiu, dentro do callback. Antes disso, address() devolve null.
O que checar na próxima vez que travar
Na ordem, do mais barato para o mais caro:
lsof -ti tcp:3000(ounetstat -ano | findstr :3000no Windows) para ter o PID.ps -p <pid> -o commandpara saber de quem é o PID antes de matar.docker ps --filter publish=3000, se o PID for do Docker.kill <pid>; só depoiskill -9 <pid>.- Se nada aparece no
lsofe o erro persiste, procure o processo por nome — pode haver um socket num endereço mais específico.
E, no código, três hábitos que fazem esse erro parar de acontecer: listen
apenas no arquivo de entrada, porta vinda de process.env.PORT, e um
on('error') que explica em vez de despencar.
Reproduza de propósito: suba dois servidores na mesma porta, use lsof ou
netstat para localizar o PID e confirme o comando antes de encerrá-lo. Depois
suba o segundo com PORT=3001 e, num teste, com porta 0. A prática termina
quando você consegue distinguir uma porta fixa ocupada de uma porta efêmera
escolhida pelo sistema.
O passo seguinte é entender o servidor por dentro, sem framework: a lição
criar um servidor HTTP no Node mostra o que
createServer e listen realmente fazem, e
o que é uma API REST coloca as rotas em cima
disso. Se você quiser o caminho inteiro em ordem, ele está no
guia de Node.js.
Prefere aprender em vídeo?
Tem uma aula sobre este assunto no nosso canal.
Perguntas frequentes
Reiniciar o computador resolve o EADDRINUSE?
Posso rodar dois servidores na mesma porta se um for HTTP e o outro HTTPS?
Por que às vezes a porta demora a liberar depois que eu paro o servidor?
Qual porta usar para não bater com nada?
Preciso de sudo para usar a porta 80?
Dúvidas e comentários
Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.
Entrar para perguntarÉ o mesmo login gratuito dos cursos.
Nenhuma dúvida por aqui ainda — a primeira pode ser a sua.
Todo o código deste artigo foi executado em Node 24.16.0 no macOS e Node 24.19.0 em container Alpine, e as saídas exibidas são as reais — como produzimos este conteúdo.
Fontes consultadas
- Node.js — net.Server.listen() — nodejs.org
- Node.js — Common system errors — nodejs.org
- Express 5 — app.listen() — expressjs.com
- man 2 bind — EADDRINUSE — man7.org



