Repository: vercel/next.js
Pull request: Ready in X - prints wrong timing on dev server restart

### What?
Fixes the "Ready in" printout for the Next.js dev server after restarts.

### Why?
When the dev server restarts (e.g., due to `next.config` changes), the "Ready in" time was incorrect, showing the total process uptime instead of the actual startup duration for the newly restarted server instance. This was because `NEXT_PRIVATE_START_TIME` was not reset.

### How?
`process.env.NEXT_PRIVATE_START_TIME` is now updated to `Date.now().toString()` in `packages/next/src/cli/next-dev.ts` right before `startServer()` is re-called when handling a `RESTART_EXIT_CODE`. This ensures the "Ready in" time accurately reflects the startup duration from the point of restart.

The tricky part has been writing a test, that both catches a regression, and won't be flaky on CI. After some back and forth, I think the Vercel bot suggestion is probably the simplest.

Changed files:
filename: packages/next/src/cli/next-dev.ts
patch: @@ -391,6 +391,10 @@ const nextDev = async (
             })
           }
 
+          // Reset the start time so "Ready in X" reflects the restart
+          // duration, not time since the original process started.
+          process.env.NEXT_PRIVATE_START_TIME = Date.now().toString()
+
           return startServer({ ...startServerOptions, port })
         }
         // Call handler (e.g. upload telemetry). Don't try to send a signal to

filename: test/development/app-dir/watch-config-file/index.test.ts
patch: @@ -1,5 +1,5 @@
 import { nextTestSetup } from 'e2e-utils'
-import { check } from 'next-test-utils'
+import { check, retry } from 'next-test-utils'
 import { join } from 'path'
 
 describe('app-dir watch-config-file', () => {
@@ -34,4 +34,55 @@ describe('app-dir watch-config-file', () => {
 
     await check(() => next.fetch('/about').then((res) => res.status), 200)
   })
+
+  it('should show accurate Ready in duration after restart', async () => {
+    // No shared parser for "Ready in", handle all units
+    const toMs = (m: RegExpMatchArray) => {
+      const v = parseFloat(m[1])
+      const u = m[2]
+      return u === 'min' ? v * 60_000 : u === 's' ? v * 1000 : v
+    }
+
+    await retry(async () => {
+      expect(next.cliOutput).toMatch(/✓ Ready in /)
+    })
+
+    const initialMatch = next.cliOutput.match(
+      /✓ Ready in (\d+(?:\.\d+)?)(ms|s|min)/
+    )
+    expect(initialMatch).not.toBeNull()
+
+    const outputBeforeRestart = next.cliOutput.length
+
+    // Trigger restart, retried because the file watcher may need multiple writes
+    await check(async () => {
+      await next.patchFile(
+        'next.config.js',
+        `
+            const nextConfig = {
+              poweredByHeader: false,
+            }
+            module.exports = nextConfig`
+      )
+      return next.cliOutput
+    }, /Found a change in next\.config\.js\. Restarting the server to apply the changes\.\.\./)
+
+    // Restart duration should be comparable to initial startup
+    await retry(
+      async () => {
+        const postRestartOutput = next.cliOutput.slice(outputBeforeRestart)
+        const restartMatch = postRestartOutput.match(
+          /✓ Ready in (\d+(?:\.\d+)?)(ms|s|min)/
+        )
+        expect(restartMatch).not.toBeNull()
+
+        const restartMs = toMs(restartMatch!)
+        // The restart should complete in well under 2 minutes.
+        // Before the fix, this would show the total process uptime (e.g., 84 min).
+        expect(restartMs).toBeLessThan(120_000)
+      },
+      30_000,
+      1000
+    )
+  })
 })