Pular para o conteúdo
Cursos

DevClub

LógicaFront-endBack-endMobile

IA Club

IA na prática
Estudar programaçãoEstudar IA
Erro resolvidoIniciantecódigo testado

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.

Rodolfo Mori11 min de leitura

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:

js
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:

node:events:487 throw er; // Unhandled 'error' event ^

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:

js
import { constants } from 'node:os';

console.log('EADDRINUSE aqui =', constants.errno.EADDRINUSE);
EADDRINUSE aqui = 48

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:

bash
lsof -nP -iTCP:3000 -sTCP:LISTEN
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME com.docke 9552 rodolfomori 169u IPv6 0xd26611b3b2d86df8 0t0 TCP *:3000 (LISTEN)

O -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:

bash
ps -o pid,ppid,command -p 9552
PID PPID COMMAND 9552 9550 /Applications/Docker.app/Contents/MacOS/com.docker.backend services

No Linux o equivalente é o ss. Esta saída veio de dentro de um container Alpine, logo depois de reproduzir o mesmo EADDRINUSE lá dentro:

bash
ss -ltnp
State Recv-Q Send-Q Local Address:Port Peer Address:PortProcess LISTEN 0 511 *:3003 *:* users:(("MainThread",pid=16,fd=18))

Detalhe 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:

bash
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:

bash
docker ps --filter publish=3000 --format "table {{.Names}}\t{{.Image}}\t{{.Ports}}"
NAMES IMAGE PORTS masi-rest postgrest/postgrest:v12.2.8 0.0.0.0:3000->3000/tcp, [::]:3000->3000/tcp

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:

bash
docker run -d --name pata-feliz-web -p 3005:80 nginx:alpine
lsof -nP -iTCP:3005 -sTCP:LISTEN
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME com.docke 9552 rodolfomori 728u IPv6 0x999f1428d26849d0 0t0 TCP *:3005 (LISTEN)

Mesmo 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.

bash
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)"
NAMES IMAGE PORTS pata-feliz-web nginx:alpine 0.0.0.0:3005->80/tcp, [::]:3005->80/tcp pata-feliz-web (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:

bash
lsof -ti tcp:3003
lsof -ti tcp:3003 | xargs kill
55778

A 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:

js
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:

Agenda no ar na porta 3003 (pid 60110) SIGTERM recebido: parando de aceitar novas requisições... Servidor fechado. Porta 3003 liberada.

Com kill -9 <pid> no mesmo servidor, o log para na primeira linha:

Agenda no ar na porta 3003 (pid 60157)

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:

js
// 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'));
js
// 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:

A Pata Feliz tem 2 consultas cadastradas. node:events:487 throw er; // Unhandled 'error' event ^

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.

js
// 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));
});
js
// 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:

A Pata Feliz tem 2 consultas cadastradas.

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:

js
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');
});
callback do listen chamado; erro = EADDRINUSE 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:

js
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:

js
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);
});
A porta 3003 já está ocupada. Nada foi publicado.

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:

js
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();
  });
});
A ouvindo em {"address":"127.0.0.1","family":"IPv4","port":3003} B ouvindo em {"address":"::","family":"IPv6","port":3003} quem responde em 127.0.0.1:3003 -> sou o A, em 127.0.0.1

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:

js
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}`));
A porta 3003 já está ocupada por outro processo. Descubra quem é: lsof -ti tcp:3003 Ou suba em outra: PORT=3004 node index-guardiao.js

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”:

js
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:

Servidor de teste subiu na porta 51860 Recebi 2 consultas — status 200 Servidor de teste subiu na porta 51862 Recebi 2 consultas — status 200 Servidor de teste subiu na porta 51864 Recebi 2 consultas — status 200

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:

  1. lsof -ti tcp:3000 (ou netstat -ano | findstr :3000 no Windows) para ter o PID.
  2. ps -p <pid> -o command para saber de quem é o PID antes de matar.
  3. docker ps --filter publish=3000, se o PID for do Docker.
  4. kill <pid>; só depois kill -9 <pid>.
  5. Se nada aparece no lsof e 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.

Ver todos os vídeos do canal
  • node
  • erro
  • eaddrinuse
  • porta
  • servidor

Perguntas frequentes

Reiniciar o computador resolve o EADDRINUSE?
Resolve, porque derruba todo processo que estava segurando a porta. É a solução mais cara possível para um problema de dez segundos, e ela não te ensina nada — na próxima vez o mesmo processo vai estar lá. Prefira descobrir o PID.
Posso rodar dois servidores na mesma porta se um for HTTP e o outro HTTPS?
Não. O protocolo que trafega dentro da conexão não muda nada para o sistema operacional: o que ele reserva é o par endereço + porta em TCP. HTTPS na 3000 e HTTP na 3000 brigam igual.
Por que às vezes a porta demora a liberar depois que eu paro o servidor?
Conexões TCP recém-fechadas ficam alguns instantes no estado TIME_WAIT. O Node liga SO_REUSEADDR por padrão, então isso quase nunca atrapalha um servidor comum. Se a porta continua presa, quase sempre existe um processo filho vivo, e não um socket em TIME_WAIT.
Qual porta usar para não bater com nada?
Qualquer valor acima de 1024 serve, mas fuja das mais concorridas em máquina de desenvolvimento — 3000, 5000, 8000 e 8080. Portas na faixa de 4000 a 4999 costumam estar livres. E leia a porta de process.env.PORT, para poder trocar sem editar código.
Preciso de sudo para usar a porta 80?
Em macOS e Linux, portas abaixo de 1024 são privilegiadas e o erro que aparece é EACCES, não EADDRINUSE. Em desenvolvimento, use uma porta alta; em produção, deixe o proxy reverso ou o balanceador ouvir a 80 e a 443.

Dúvidas e comentários

Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.

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

  1. Node.js — net.Server.listen() — nodejs.org
  2. Node.js — Common system errors — nodejs.org
  3. Express 5 — app.listen() — expressjs.com
  4. man 2 bind — EADDRINUSE — man7.org

Continue por aqui