Fix "EADDRINUSE: address already in use :::3000" Next.js
Change the default Next.js port when it collides with another process. Step‑by‑step fix with code, verification, and prevention tips.
TL;DR#
If you're seeing listen EADDRINUSE: address already in use :::3000, the cause is that another process already occupies the default Next.js port. Fix it by telling Next.js to listen on a free port via an environment variable, a CLI flag, or a small server wrapper.
If that doesn't work, scroll to verify the fix — there are two common variants this guide also covers.
What you'll see#
When you run the default development command, the terminal stops with a stack trace that looks like this:
> next dev
Error: listen EADDRINUSE: address already in use :::3000
at Server.setupListenHandle [as _listen2] (node:net:1315:19)
at listenInCluster (node:net:1385:12)
at Server.listen (node:net:1475:7)
at Server.listen (node_modules/next/dist/server/next-server.js:1234:15)
at Object.<anonymous> (node_modules/next/dist/bin/next.js:45:23)
at Module._compile (node:internal/modules/cjs/loader:1126:14)
at Object.Module._extensions..js (node:internal/modules/cjs/loader:1180:10)It happens right after you execute npm run dev (or yarn dev) on a fresh clone of a Next.js project. The behavior is identical on macOS, Linux, and Windows because the underlying Node.js net module reports the same error code across platforms.
Root cause#
Next.js ships with a built‑in development server that listens on TCP port 3000 unless you tell it otherwise. When the operating system reports EADDRINUSE, it means the bind call failed because something else already owns that socket. Common culprits are:
- A local API server (Express, Fastify, Supabase Edge Functions) running on the same port.
- A Docker container exposing port 3000.
- A previous instance of
next devthat didn’t shut down cleanly.
Because the error bubbles up during the server startup phase, the process exits before any page rendering occurs, leaving you with the stack trace above and no hot‑reload.
Next.js resolves the port from the -p/--port flag first, then the PORT environment variable, then falls back to a hard-coded 3000 default. There is no port option in next.config.js, which is why a config or .env file has no effect:
# Override the default 3000 — via the CLI flag…
next dev -p 4000
# …or the PORT environment variable
PORT=4000 next devThe fix#
There are three idiomatic ways to override the default port. I’ll show each with a concrete snippet, then explain why they work.
1. Inline shell environment variable#
Pass PORT directly on the command line — before the next dev command. Next.js's HTTP server starts before .env* files are loaded, so setting PORT inside .env.local has no effect on the listening port.
macOS / Linux (bash/zsh):
PORT=4000 next devWindows PowerShell:
$env:PORT=4000; next devWindows CMD:
set PORT=4000 && next devYou can also export the variable at the OS level (e.g. in ~/.zshrc or via System Properties → Environment Variables on Windows) so every project in your shell inherits it without touching package.json.
2. CLI flag#
Modify the dev script in package.json to pass the -p flag:
{
"scripts": {
"dev": "next dev -p 4000",
"build": "next build",
"start": "next start"
}
}The -p (or --port) flag is parsed by the Next.js binary and overrides any environment variable.
3. Custom server wrapper (useful for complex setups)#
If you need to run additional middleware (e.g., Supabase auth) before handing control to Next.js, create server.js:
// server.js
const { createServer } = require('http');
const next = require('next');
const dev = process.env.NODE_ENV !== 'production';
const app = next({ dev });
const handle = app.getRequestHandler();
const PORT = parseInt(process.env.PORT, 10) || 4000;
app.prepare().then(() => {
createServer((req, res) => {
// Example: inject a header for every request
res.setHeader('X-Custom-Header', 'iloveblogs');
handle(req, res);
}).listen(PORT, (err) => {
if (err) throw err;
console.log(`> Custom server listening on http://localhost:${PORT}`);
});
});Then change the dev script:
{
"scripts": {
"dev": "node server.js",
"build": "next build",
"start": "next start"
}
}All three approaches tell the Next.js runtime to bind to 4000 (or any port you choose) instead of the colliding 3000.
Step by step#
- Decide which method fits your workflow. For most single‑page apps, the inline shell variable is the least invasive change.
- Add the snippet above to the appropriate place (command line,
package.jsonscripts, orserver.js). - Restart the dev server with
npm run dev. - Verify the console output shows the new port.
Verify the fix#
Run the development command you just configured:
npm run devYou should see output similar to:
> my-next-app@0.1.0 dev /path/to/my-next-app
> next dev -p 4000
▲ Next.js 15.x.x
- Local: http://localhost:4000
- Network: http://192.168.x.x:4000Notice that the error stack trace is gone and the URL reflects the port you set. Open http://localhost:4000 in a browser; the app should load exactly as before.
If you still encounter EADDRINUSE, double‑check that the new port isn’t also taken. You can scan for listening ports with:
lsof -iTCP -sTCP:LISTEN -P | grep 4000If the command returns a line, stop that process (kill -9 <PID>) or choose a different port.
Variant A — Port already in use#
Sometimes the port you pick is also occupied (e.g., you chose 4000 but a local Supabase emulator runs there). In that case, repeat the verification step with a different number, such as 5000. The same code changes apply; only the numeric value changes.
Variant B — Running in Docker#
When you containerize the app, the host‑side port mapping (docker run -p 3000:3000) may conflict with the container’s internal port. The fix is to expose a different internal port in Dockerfile or docker-compose.yml and keep the Next.js configuration aligned:
# docker-compose.yml
services:
web:
build: .
ports:
- "5000:4000" # host:container
environment:
- PORT=4000Now the container listens on 4000 internally while the host maps it to 5000.
Why this happens (and how to avoid it next time)#
The root invariant is that only one process can bind to a given TCP port on a host at a time. Because Next.js defaults to a well‑known development port, the likelihood of a clash grows as you add more local services (Supabase, Redis, mock APIs). To stay ahead:
- Add a
PORTentry to.env.exampleso every teammate knows which variable to set. - Include a pre‑flight script in
package.jsonthat checks port availability:
// scripts/check-port.js
const net = require('net');
const port = parseInt(process.env.PORT, 10) || 3000;
const server = net.createServer();
server.once('error', err => {
if (err.code === 'EADDRINUSE') {
console.error(`Port ${port} is already in use. Choose another port or stop the conflicting process.`);
process.exit(1);
}
});
server.once('listening', () => {
server.close();
});
server.listen(port);Then run it before next dev:
{
"scripts": {
"predev": "node scripts/check-port.js",
"dev": "npm run predev && next dev"
}
}- Document the chosen port in your onboarding guide and reference it when you write about deployment. For example, see my article on Deploying Next.js + Supabase to Production where I lock the production port to
8080and keep the dev port configurable.
By making the port explicit and checking it early, you eliminate the surprise EADDRINUSE crash.
Related#
Frequently Asked Questions
One email a month — no fluff
RLS gotchas, Next.js cache debugging, and the one Supabase setting that bit me last month.
Continue Reading
Supabase Auth Session Missing? Fix for Next.js (2026)
Supabase auth sessions mysteriously disappearing after page refresh? Learn the exact cause and fix it in 5 minutes with this tested solution.
Fix ChunkLoadError: Loading Chunk Failed in Next.js
"ChunkLoadError: Loading chunk 5760 failed" almost always means a user has an old tab open after you shipped a new deploy. The fix is configuration — deploymentId, build IDs, and CDN headers — not a try/catch.
Fix next/image Hostname Not Configured in Next.js
next/image strictly allow-lists remote hosts. The fix is images.remotePatterns in next.config — matched exactly on protocol, hostname, port, and pathname. Get one wrong (http vs https, a subdomain, a missing port) and it still blocks.
Browse by Topic
Find stories that matter to you.
