Repository: vercel/next.js
Pull request: fix: use signal-based exit codes to prevent inspector blocking exit

## What?

Use signal-based exit codes when shutting down the Next.js server to prevent the process from waiting for the debugger to disconnect.

## Why?

When Node.js devtools are attached (via `--inspect` flag or the Next.js DevTools "Attach debugger" feature), pressing Ctrl+C causes the dev server to hang instead of exiting immediately. This happens because calling `process.exit(0)` makes Node.js treat it as a normal exit, which waits for all debuggers to disconnect before terminating.

In contrast, plain Node.js terminates immediately on SIGINT even with a debugger attached, because signal-based termination doesn't wait for debuggers.

## How?

Instead of calling `process.exit(0)` after cleanup, use signal-based exit codes:

- SIGINT (signal 2) → `process.exit(130)` (128 + 2)
- SIGTERM (signal 15) → `process.exit(143)` (128 + 15)

This tells Node.js the process was terminated by a signal, which avoids the debugger wait behavior.

Changes:
- Modified the cleanup function to accept the signal that triggered it
- Use `process.exit(signal === 'SIGINT' ? 130 : 143)` instead of `process.exit(0)`
- Removed the previous `inspector.close()` workaround as it's no longer needed

Changed files:
filename: packages/next/src/server/lib/start-server.ts
patch: @@ -382,7 +382,7 @@ export async function startServer(
       try {
         let cleanupStarted = false
         let closeUpgraded: (() => void) | null = null
-        const cleanup = () => {
+        const cleanup = (signal: 'SIGINT' | 'SIGTERM') => {
           if (cleanupStarted) {
             // We can get duplicate signals, e.g. when `ctrl+c` is used in an
             // interactive shell (i.e. bash, zsh), the shell will recursively
@@ -435,7 +435,23 @@ export async function startServer(
             }
 
             debug('start-server process cleanup finished')
-            process.exit(0)
+
+            // Exit with signal-based exit code (128 + signal number) so that
+            // Node.js treats this as a signal termination, not a normal exit.
+            // This avoids waiting for the debugger to disconnect.
+            switch (signal) {
+              case 'SIGINT':
+                process.exit(130)
+                break
+              case 'SIGTERM':
+                process.exit(143)
+                break
+              default:
+                // Make sure all handled signals have explicit exit codes.
+                // This is just a fallback to guard against unsound types.
+                signal satisfies never
+                process.exit(128)
+            }
           })()
         }
 

filename: test/integration/cli/test/index.test.ts
patch: @@ -105,7 +105,8 @@ describe('CLI Usage', () => {
           await testExitSignal(
             'SIGINT',
             ['start', dirBasic, '-p', port],
-            /- Local:/
+            /- Local:/,
+            130 // 128 + 2 (SIGINT)
           )
         })
         test('should exit when SIGTERM is signalled', async () => {
@@ -122,7 +123,8 @@ describe('CLI Usage', () => {
           await testExitSignal(
             'SIGTERM',
             ['start', dirBasic, '-p', port],
-            /- Local:/
+            /- Local:/,
+            143 // 128 + 15 (SIGTERM)
           )
         })
 

filename: test/production/graceful-shutdown/index.test.ts
patch: @@ -208,9 +208,9 @@ function runTests(dev = false) {
         expect(res.status).toBe(200)
         expect(await res.json()).toStrictEqual({ hello: 'world' })
 
-        // App finally shuts down
-        expect(await appKilledPromise).toEqual([0, null])
-        expect(app.exitCode).toBe(0)
+        // App finally shuts down with signal-based exit code (128 + 15 for SIGTERM)
+        expect(await appKilledPromise).toEqual([143, null])
+        expect(app.exitCode).toBe(143)
       })
 
       it('should stop accepting new requests when shutting down', async () => {
@@ -225,9 +225,9 @@ function runTests(dev = false) {
           1000
         )
 
-        // App finally shuts down
-        expect(await appKilledPromise).toEqual([0, null])
-        expect(app.exitCode).toBe(0)
+        // App finally shuts down with signal-based exit code (128 + 15 for SIGTERM)
+        expect(await appKilledPromise).toEqual([143, null])
+        expect(app.exitCode).toBe(143)
       })
     })
   }