Skip to content

fix(dev): forcibly close websocket connections when quitting - #1532

Merged
danielroe merged 3 commits into
nuxt:mainfrom
userquin:fix-quit-timeout-on-windows
Sep 26, 2026
Merged

danielroe merged 3 commits into
nuxt:mainfrom
userquin:fix-quit-timeout-on-windows

Conversation

@userquin

@userquin userquin commented Sep 10, 2026 •

Copy link
Copy Markdown
Member

🔗 Linked issue

resolves #1531

📚 Description

This PR includes:

Maybe we can fix the deadlock here, not yet sure...

@pkg-pr-new

pkg-pr-new Bot commented Sep 10, 2026 •

Copy link
Copy Markdown
  • nuxt-cli-playground

    npm i https://pkg.pr.new/create-nuxt@1532
    
    npm i https://pkg.pr.new/nuxi@1532
    
    npm i https://pkg.pr.new/@nuxt/cli@1532
    

commit: 721ff7f

@github-actions

github-actions Bot commented Sep 10, 2026 •

Copy link
Copy Markdown
Contributor

CLI benchmark

@nuxt/cli v4.0.0-alpha.1 (baseline) vs v4.0.0-alpha.1 (this PR)

Metric baseline v4.0.0-alpha.1 head v4.0.0-alpha.1 Delta
nuxt --version wall time (median) 71 ms 70 ms -0.7%
nuxt --help wall time (median) 156 ms 156 ms -0.5%
nuxt dev --help wall time (median) 111 ms 112 ms +1.2%
nuxt --version modules loaded 38 38 0.0%
nuxt --help modules loaded 144 144 0.0%
nuxt dev --help modules loaded 63 63 0.0%
Installed node_modules 2.39 MB 2.39 MB +0.0%
Published tarball (packed) 236.0 kB 236.0 kB +0.0%
Full report

@nuxt/cli v4.0.0-alpha.1 (baseline) vs v4.0.0-alpha.1 (head)

Setting Value
Baseline ref:7415659f139227d558a5904b930fa7b493104e30 (v4.0.0-alpha.1)
Head local packages/nuxt-cli at c618148 (v4.0.0-alpha.1)
Node v24.21.0
OS Linux 6.17.0 (kernel 6.17.0-1022-azure)
CPU AMD EPYC 7763 64-Core Processor x 4
Memory 15.6 GB
Load average at start 0.78, 0.23, 0.08
Run started 2026-09-26T13:05:58.505Z

Cold CLI startup

Median of 15 interleaved runs per command, one warmup discarded.

Command baseline v4.0.0-alpha.1 median head v4.0.0-alpha.1 median Delta baseline v4.0.0-alpha.1 min / p95 head v4.0.0-alpha.1 min / p95
nuxt --version 71 ms 70 ms -0.7% 69 ms / 74 ms 67 ms / 73 ms
nuxt --version (first output byte) 66 ms 66 ms -0.8% 64 ms / 69 ms 62 ms / 69 ms
nuxt --help 156 ms 156 ms -0.5% 152 ms / 161 ms 152 ms / 163 ms
nuxt --help (first output byte) 151 ms 150 ms -0.6% 146 ms / 155 ms 146 ms / 157 ms
nuxt dev --help 111 ms 112 ms +1.2% 107 ms / 117 ms 108 ms / 125 ms
nuxt dev --help (first output byte) 105 ms 107 ms +1.6% 101 ms / 111 ms 103 ms / 120 ms
nuxt <unknown-command> (no-op) 168 ms 168 ms +0.1% 163 ms / 177 ms 162 ms / 174 ms
nuxt <unknown-command> (no-op) (first output byte) 161 ms 162 ms +0.5% 157 ms / 171 ms 156 ms / 168 ms

Module load cost

Counted with a module.registerHooks load hook, compile cache disabled. Counts every JS module actually evaluated on that code path (built-ins excluded, native addons excluded).

Command baseline v4.0.0-alpha.1 modules head v4.0.0-alpha.1 modules Delta baseline v4.0.0-alpha.1 source bytes head v4.0.0-alpha.1 source bytes Delta
nuxt --version 38 38 0.0% 296.7 kB 296.7 kB 0.0%
nuxt --help 144 144 0.0% 958.9 kB 958.9 kB 0.0%
nuxt dev --help 63 63 0.0% 449.8 kB 449.8 kB 0.0%

Install footprint and published tarball

Each version installed on its own into an empty project with nothing but @nuxt/cli as a dependency, so the tree is exactly the CLI and its transitive dependencies. npm cache is warm and the registry is only consulted for metadata, so install wall time is indicative, not a network benchmark.

Metric baseline v4.0.0-alpha.1 head v4.0.0-alpha.1 Delta
Direct dependencies of @nuxt/cli 22 22 0.0%
Packages in the installed tree (unique name@version) 38 38 0.0%
Unique package names 38 38 0.0%
Package directories on disk (cross-check) 31 31 0.0%
Installed node_modules on disk 2.39 MB 2.39 MB +0.0%
Installed files 420 420 0.0%
Install wall time (warm npm cache, median of 3) 1.37 s 1.38 s +1.1%
Published tarball (packed) 236.0 kB 236.0 kB +0.0%
Published tarball (unpacked) 769.0 kB 769.0 kB +0.0%
Files in tarball 97 97 0.0%

Interleaved runs on a shared runner: trust the deltas, not the absolute timings. The dev, restart and build suites run locally via pnpm bench:cli.

@codspeed

codspeed Bot commented Sep 10, 2026 •

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 2 untouched benchmarks


Comparing userquin:fix-quit-timeout-on-windows (721ff7f) with main (7415659)

Open in CodSpeed

@codecov-commenter

codecov-commenter commented Sep 10, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 75.00000% with 1 line in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (main@7415659). Learn more about missing BASE report.

Files with missing lines Patch % Lines
packages/nuxt-cli/src/dev/utils.ts 75.00% 1 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main    #1532   +/-   ##
=======================================
  Coverage        ?   83.39%           
=======================================
  Files           ?      166           
  Lines           ?    10944           
  Branches        ?     3170           
=======================================
  Hits            ?     9127           
  Misses          ?     1543           
  Partials        ?      274           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@userquin

userquin commented Sep 10, 2026 •

Copy link
Copy Markdown
Member Author

Nuxt cli timeout: there is a bug at nitropack (nitro v2.13.4), when closing the worker, if there is a timeout, the caller will hang forever, in this case, nuxt cli will timeout with its default (15 seconds), missing resolve call on setTimeout at #closeWorker.

The #closeWorker is fine at nitro v3.

Patching nitropack on my local node_modules calling the resolve method inside the setTimeout:
teemirnal showing nuxt cli closes gracefully with the nitropack patch but with 5 seconds of time

@userquin

userquin commented Sep 10, 2026 •

Copy link
Copy Markdown
Member Author

Found the issue, it is a dead lock with nitropack 🥳 🥳 🥳

@userquin

userquin commented Sep 10, 2026 •

Copy link
Copy Markdown
Member Author

The dead lock

When shutting down the dev server (nitro dev-server at nitropack), the process often delays for 5 seconds until it throws WARN force closing dev worker..., ultimately failing to gracefully close the IPC sockets.

Root Cause

There is a deadlock between the dev worker and the parent proxy during shutdown.
Inside the dev worker's shutdown sequence, it executes await new Promise((resolve) => listener?.close(resolve)). The listener.close() method waits for all active connections to drain before resolving. However, the parent process holds keep-alive/upgrade connections open and waits for the worker to send the { event: "exit" } message before destroying its proxy sockets.

  • The worker waits for the parent to disconnect.
  • The parent waits for the worker to send "exit".

This results in an infinite loop that only breaks when the parent's 5-second graceful shutdown timeout forcefully terminates the worker thread.

The Fix at nitropack

By calling listener?.close() asynchronously without wrapping it in an await Promise, the shutdown sequence proceeds and immediately sends the "exit" event to the parent. The parent receives the signal, successfully closes the proxy connections, and the listener naturally drains and closes in milliseconds. This completely eliminates the 5-second delay and prevents orphaned resources.

Changes at nitro-dev.ts shutdown

async function shutdown() {
  server.closeAllConnections?.();
  listener?.close();
  await nitroApp.hooks.callHook("close").catch(console.error);
  parentPort?.postMessage({ event: "exit" });
}

Applied these changes on my local:

async function shutdown() {
  console.log('🛑 [Worker] Shutdown started...');
  server.closeAllConnections?.();
  listener?.close();
  await nitroApp.hooks.callHook("close").then(() => console.log('🛑 [Worker] Nitro hooks called')).catch(console.error);
  console.log('🛑 [Worker] Sending "exit" event to the parent...');
  parentPort?.postMessage({ event: "exit" });
}
Nuxt cli quit correctly

@userquin
userquin marked this pull request as ready for review September 10, 2026 22:20
@userquin
userquin requested a review from danielroe as a code owner September 10, 2026 22:20
@coderabbitai

coderabbitai Bot commented Sep 10, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

Warning

Review limit reached

Next included review available in 1 minute.

Check out review usage here.

View limit details

Limit details: You’ve used all 2 included reviews currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: b1bcb456-983e-4df3-b257-13a9453f84b3

📥 Commits

Reviewing files that changed from the base of the PR and between 3cc3ae6 and 721ff7f.

📒 Files selected for processing (2)
  • packages/nuxt-cli/src/dev/utils.ts
  • packages/nuxt-cli/test/unit/dev/lifecycle.spec.ts

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: d71005bc-4760-4139-b991-e20131371e26

📥 Commits

Reviewing files that changed from the base of the PR and between dc2c208 and 3cc3ae6.

📒 Files selected for processing (2)
  • packages/nuxt-cli/src/dev/utils.ts
  • packages/nuxt-cli/test/unit/dev/lifecycle.spec.ts

Included review availability: This review used your included allowance. Your plan provides up to 2 included reviews per hour; 1 remain after this review.


📝 Walkthrough

Walkthrough

The development server now tracks each WebSocket immediately after acceptance. The tracking occurs before Vite HMR filtering and Nitro upgrade handling. Each socket is removed from the tracking set when it closes, so shutdown cleanup can destroy connections that bypass request handling.

Priority: ➖ Normal

Estimated code review effort: 2 (Simple) | ~10 minutes

Severity of issue fixed: Medium

Merge Risk: ⚪ Minimal · up to 3cc3a

The change closes tracked upgraded connections before Nuxt shuts down, and the lifecycle test exercises that sequence. No concrete merge-blocking risk remains in the reviewed scope.

Security Architecture Review

Security architecture risk: 🔵 Low · up to 3cc3a

The change closes tracked development-server connections earlier to prevent shutdown hangs. Review found no new network entrypoint or weakened upgrade control, though concurrent shutdown behavior is not directly tested.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The changed behavior affects sockets tracked by this development-server instance during shutdown or reload, rather than expanding who can reach its upgrade listener.

Trust Boundaries and Controls

  • observed — The existing active-server guard, HMR path check, and Nuxt upgrade delegation remain in place; the added call changes cleanup timing, not upgrade authorization or routing.

Resilience and Maintainability Implications

  • inferred — Clearing the tracked set before Nuxt close hooks run reduces dependence on those hooks to terminate an already open socket. The new test covers one such connection, not concurrent upgrades or failure paths.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Issue #1531 requires the upgraded WebSocket connection to enter shutdown tracking and close during server shutdown. The PR updates NuxtDevServer.close() to destroy tracked WebSocket connections befo…
Out of Scope Changes check ✅ Passed The source changes manage tracked WebSocket connections during NuxtDevServer.close(). The added lifecycle test verifies the shutdown order for an active upgraded socket. These changes directly suppo…
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 2…
Title check ✅ Passed The title clearly and concisely describes the main change: forcibly closing WebSocket connections during development server shutdown.
Description check ✅ Passed The description is related to the changeset. It explains tracking Vite HMR connections and resolving shutdown hangs, which matches the implementation and tests.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@userquin

userquin commented Sep 10, 2026 •

Copy link
Copy Markdown
Member Author

The changes in this PR won't be necessary if nitropack change shutdown logic:

nuxt cli quit command without changes in this pr but nitrpack patched

@userquin
userquin marked this pull request as draft September 10, 2026 22:46
@danielroe

danielroe commented Sep 17, 2026 •

Copy link
Copy Markdown
Member

this seems like a nitro bug, rather than something that should be worked around here

cc: @pi0

@danielroe
danielroe marked this pull request as ready for review September 26, 2026 12:02

@danielroe danielroe left a comment •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thank you so much for testing and spotting this! ❤️

I think it's right to hotfix here to avoid shipping a broken cli for v4... I'll be opening a PR to nitro to fix the upstream issues.

@danielroe danielroe changed the title fix(dev): quit command hangs/timeouts on Windows fix(dev): forcibly close websocket connections when quitting Sep 26, 2026
@danielroe
danielroe enabled auto-merge September 26, 2026 12:05
@danielroe
danielroe disabled auto-merge September 26, 2026 13:37
@danielroe
danielroe merged commit 9b70cb6 into nuxt:main Sep 26, 2026
17 checks passed
@github-actions github-actions Bot mentioned this pull request Sep 26, 2026
@userquin
userquin deleted the fix-quit-timeout-on-windows branch September 26, 2026 14:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

alpha version: quit command hangs/timeouts on Windows 10 PRO

3 participants