← Back to blog

Guide

Next.js port 3000 in use: pinning the port and finding the leftover server

September 14, 20263 min read

Next.js does not stop when port 3000 is taken. It tells you it is using another one and carries on:

⚠ Port 3000 is in use, trying 3001 instead.

Convenient, and the source of a specific afternoon of confusion.

Why moving quietly is a problem

Half your setup assumes 3000. An OAuth provider with http://localhost:3000/api/auth/callback in its allowlist. A CORS config on your API. A NEXTAUTH_URL in .env. A Stripe webhook forwarder. A teammate’s bookmark.

Move the app to 3001 and none of those follow. The symptoms are not “wrong port”, they are “login is broken” and “the API returns CORS errors”, which sends you looking in entirely the wrong place.

So when something that worked yesterday breaks today, read the first lines of your dev server output before you debug anything else.

Pin the port

Tell Next which port you want:

next dev -p 3000

Or make it permanent in package.json so nobody has to remember:

{
  "scripts": {
    "dev": "next dev -p 3000"
  }
}

This still falls back if 3000 is taken, so it is about being explicit rather than forcing a failure. The real protection is noticing, which is the next part.

Find and clear whatever holds 3000

lsof -i :3000

Then:

lsof -ti :3000 | xargs kill

-t prints just the process IDs and xargs hands all of them to one kill, which matters because a Next dev server can involve more than one process. Plain kill sends SIGTERM and lets it shut down; add -9 only if it ignores you.

The usual suspects

Another Next app. Every Next project defaults to 3000, so two checkouts collide by design. Give each project a fixed, different port in package.json and this stops happening.

A previous run that did not exit. Closing the terminal rather than pressing Ctrl+C is the common path. So is a crash during a hot reload.

Turbopack or the build worker lingering. After a hard crash you can be left with a node process holding the port with no visible terminal attached to it.

Docker. If you containerise the app in development, a container publishing 3000 holds it from outside Node entirely. Check before killing:

docker ps --filter "publish=3000"

If that returns a container, docker stop it. Killing docker-proxy achieves nothing, Docker just puts it back.

When 3000 is busy but nothing is listening

Occasionally you get the conflict and lsof -i :3000 prints nothing at all. That is TIME_WAIT, a TCP state where a recently closed connection keeps the port reserved for around thirty seconds. There is no process to kill. Wait, or use another port for a minute.

We cover that in detail in what EADDRINUSE actually means.

Checking before it bites

The cheapest habit is to glance at the port before you start, rather than after something breaks:

lsof -i :3000

Nothing printed means you are clear.

Bosun keeps that answer in the menu bar permanently, with the process, container or tunnel behind each port named properly. macOS 14 or later, 14 day trial, no account.

See this instead of typing it

Bosun lives in your menu bar and shows every open port on your Mac, live, mapped to the process behind it. One-click kill, SIGTERM first. Useful the first time this happens. Genuinely useful the fifth time it happens in one afternoon.