How to Change the Port in Next.js (dev and start)
Change the default Next.js port when it collides with another process, and know why next dev sometimes moves to 3001 on its own while next start crashes.
Next.js binds its server to TCP port 3000 unless you tell it otherwise, and on a machine that also runs a local API, a Docker container or a forgotten previous server, that default is where collisions happen. This post covers the two built-in ways to move the port (the -p flag and the PORT variable), a custom server, and what the EADDRINUSE crash actually looks like. Every output below was reproduced on Next.js 16.3.8 on 2 October 2026.
listen EADDRINUSE: address already in use :::3000#
With port 3000 already taken, next start, or next dev started with PORT=3000 or -p 3000, prints this and exits:
⨯ Failed to start server
Error: listen EADDRINUSE: address already in use :::3000
at <unknown> (Error: listen EADDRINUSE: address already in use :::3000)
at new Promise (<anonymous>) {
code: 'EADDRINUSE',
errno: -4091,
syscall: 'listen',
address: '::',
port: 3000
}That run was on Windows. The code is EADDRINUSE on every platform; the numeric errno is the operating system's own number, so it differs on Linux and macOS.
EADDRINUSE means the bind call failed because something else already owns that port. Common culprits are:
- A local API server (Express, Fastify, Supabase Edge Functions) running on the same port.
- A Docker container publishing port 3000 on the host.
- A previous
next devornext startthat did not shut down cleanly.
Why plain next dev often does not crash#
With no -p flag and no PORT variable, next dev does not stop on a busy port. In the same reproduction, a second project started with plain next dev while 3000 was taken printed:
⚠ Port 3000 is in use by process 17688, using available port 3001 instead.
▲ Next.js 16.3.8 (Turbopack)
- Local: http://localhost:3001Two consequences. First, if your app suddenly answers on 3001, something else holds 3000. Second, the moment you set the port explicitly, this fallback is off: -p 3000 and PORT=3000 both produced the crash above. Starting a second next dev in the same project folder is refused outright, with ⨯ Another next dev server is already running.
How Next.js picks the port#
The Next.js CLI reference documents -p/--port for both next dev and next start, the PORT environment variable, and the 3000 default. It also states that PORT cannot be set in .env, because the HTTP server boots before any code is initialised. There is no port option in next.config.js. In the reproduction:
PORT=4100in.env.localwas ignored: the server started on 3000 while listing.env.localunder Environments.PORT=5000 next dev -p 4000started on 4000: the flag wins over the variable.
Option 1 — the -p flag in package.json#
This is the most reliable option, because it lives with the project and works the same in every shell:
{
"scripts": {
"dev": "next dev -p 4000",
"build": "next build",
"start": "next start -p 4000"
}
}next start takes the same flag, so set it there too if production should not use 3000. npm start serves an existing production build and does not create one, so run npm run build first, or the server stops with the missing .next/BUILD_ID error.
Option 2 — PORT in the shell#
Set PORT in the shell that launches the command, not in a .env file.
macOS / Linux (bash/zsh):
PORT=4000 npm run devWindows PowerShell:
$env:PORT=4000; npm run devOn a team with mixed operating systems, prefer Option 1: the inline PORT=4000 command syntax does not work in PowerShell or CMD.
Option 3 — a custom server.js#
A custom server binds the port itself, in its listen() call; the next() constructor does not open any port. Listen for 'error' on the server: the callback passed to listen() is only a 'listening' handler and never receives an error, per the Node.js server.listen() docs.
// 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(() => {
const server = createServer((req, res) => handle(req, res));
server.on('error', (err) => {
if (err.code === 'EADDRINUSE') console.error(`Port ${port} is already in use.`);
else console.error(err);
process.exit(1);
});
server.listen(port, () => {
console.log(`> Custom server listening on http://localhost:${port}`);
});
});Then point the dev script at it:
{
"scripts": {
"dev": "node server.js",
"build": "next build",
"start": "NODE_ENV=production node server.js"
}
}The start line uses the inline-variable syntax, so on Windows use cross-env or set NODE_ENV in the environment instead. A custom server also gives up some Next.js optimisations, which the custom server docs list; use it only when you need it for something else.
Confirming the new port#
Run the command you configured. With next dev -p 4000, Next.js 16.3.8 printed:
▲ Next.js 16.3.8 (Turbopack)
- Local: http://localhost:4000If you still get EADDRINUSE, the new port is taken as well. Find the process that holds it.
macOS / Linux:
lsof -iTCP:4000 -sTCP:LISTEN -P
kill <PID>Windows (PowerShell or CMD):
netstat -ano | findstr :4000
taskkill /PID <PID> /FThe last column of the netstat line is the process ID. Stop the process only if you know what it is; otherwise pick another port.
Docker: only the host side can collide#
Inside a container the app has its own network namespace, so the container's port 3000 never clashes with a process on your machine. What can clash is the host side of the mapping: docker run -p 3000:3000 fails if something on the host already listens on 3000. The Docker published ports docs describe the host:container order. Change the host number and leave the app alone:
# docker-compose.yml
services:
web:
build: .
ports:
- "5000:3000" # host 5000 → container 3000The app keeps listening on 3000 inside the container and is reachable on http://localhost:5000.
Keeping ports from colliding across the team#
Only one process can listen on a given TCP port on a host. As local services pile up (Supabase, Redis, mock APIs), make the port explicit in the dev script, as in Option 1, so everyone runs the same one. Remember that an explicit port also turns off the automatic move to 3001, so a clash fails loudly instead of silently changing the URL.
If you want an earlier, clearer message, add a check script and name it predev. npm runs a pre<name> script automatically before <name> (npm scripts docs), so the dev script must not call it again:
// scripts/check-port.js
const net = require('net');
const port = 4000;
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);{
"scripts": {
"predev": "node scripts/check-port.js",
"dev": "next dev -p 4000"
}
}In the reproduction, a dev script written as npm run predev && next dev ran predev twice. Once the app runs, deployment brings its own settings. The Next.js + Supabase production guide covers the Vercel setup, environment variables and migrations.
Further reading: Can't Import SVG in Next.js: Fix for Turbopack and Webpack, Why global-error.tsx Doesn't Fire in Next.js Dev Mode, Fix: FetchError request to localhost:1337 ECONNREFUSED ::1, Fix barrel_optimize Build Warnings with MUI in Next.js. Also see Failed to find font override values: next/font/google fix.
Frequently Asked Questions
Still stuck? Paste your error.
Paste the error message or stack trace — we check it against every fix and tool on the site.
One email a month — the latest explained errors
What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.
Continue Reading
Fix 'Next.js 15 requires Node.js 18.18' Build Error
Next.js 15 drops support for older Node versions. Here is how to upgrade to Node.js 18.18.0 or later and fix build errors.
Fix Turbopack Build Errors from a Webpack Config
Find the plugin behind the unexpected webpack hook, choose a compiler deliberately, and verify that the behaviour you need still runs.
Fix Turbopack's Wrong Workspace Root Warning
Trace the lockfile behind an unexpected workspace root, choose the right standalone or monorepo boundary, and separate a reported memory spike from a verified cause.


