quic: reuse TLS pause machinery to drop event deferral & improve 0RTT - #65522

Merged
nodejs-github-bot merged 5 commits into
nodejs:mainfrom
pimterry:pause-for-quic-0rtt
Sep 2, 2026
Merged

quic: reuse TLS pause machinery to drop event deferral & improve 0RTT#65522
nodejs-github-bot merged 5 commits into
nodejs:mainfrom
pimterry:pause-for-quic-0rtt

Conversation

@pimterry

Copy link
Copy Markdown
Member

With this change, the TLS suspend mechanism I introduced in #64827 becomes generic and we reuse it in QUIC. This adds new client hello structures, which hide the OpenSSL vs BoringSSL differences, and expose an interface for each handshake flow to continue, suspend or fail the handshake. While suspended they can access the details and validate/prepare according to their needs.

There's one immediate advantage here: this allows QUIC to reuse this existing mechanism to largely replace its event deferral system. In QUIC, we need to defer some handshake events (mostly relating to resumption & 0RTT, see #64132) until the handshake is sufficiently complete so that at the session event we can do some setup with the data already available, but before the actual data processing within starts.

To do this currently we have a whole event deferral mechanism and multi-step session ticket extraction & validation flow. Directly suspending the handshake reusing the existing mechanism from node:tls lets us delete that completely, do the simple thing instead, and get the same result.

This then has another medium-term benefit: as part of #63995 we will be adding dynamic attach, where the application protocol is decided dynamically on session creation, which requires deferring session ticket processing even further (because we don't even know which protocol to validate for). This mechanism gives us the structure in which to easily do that, giving us 0RTT + dynamic attach.

As part of this, this changes the internal flow of how ALPN & SNI are resolved for QUIC, and extracts ALPN helpers to share more logic between TLS/QUIC/DTLS. Instead of calculating them synchronously within the OpenSSL callbacks, we now precalculate them at the suspend point, and then the OpenSSL callbacks later just use the precalculated values.

This is intended to only create visible changes to node:quic. Though it touches node:tls & node:dtls, it's just commonizing logic and preserves all existing behaviour (including callback order and other details). In future I'd like to make some small breaking changes to both to bring these further inline, but that's tricky and we can ignore it for now.

With this change, the TLS pause mechanisms used in node:tls are now made
generic, and used in QUIC to pause the handshake entirely for the
session event, thereby stopping almost all events being emitted
(everything except qlog) so that all the previously existing deferral
machinery is unnecessary.
This is useful immediately to clean up & simplify things, but also
medium term because this makes the 0RTT flow significantly clearer,
which will help with the upcoming dynamic attach for QUIC (which
requires deferring ticket processing & 0RTT delivery until after
user code).
As part of this, this changes the internal flow of how ALPN & SNI are
resolved. Instead of calculating them synchronously within the OpenSSL
callbacks, we precalculate them at the pause, and then the OpenSSL
callbacks just return the precalculated values.
This is intended to only create visible changes to node:quic. Though it
touches node:tls, it preserves all existing behaviour (including
callback order and other details).
Signed-off-by: Tim Perry <pimterry@gmail.com>
@pimterry
pimterry requested a review from jasnellAugust 24, 2026 18:54
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Review requested:

  • @nodejs/crypto
  • @nodejs/gyp
  • @nodejs/quic

@nodejs-github-botnodejs-github-bot added c++ Issues and PRs that require attention from people who are familiar with C++. lib / src Issues and PRs involving general changes in the lib/ or src/ directories. needs-ci PRs that need a full CI run. labels Aug 24, 2026
@codecov

codecovBot commented Aug 24, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 60.00000% with 46 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.11%. Comparing base (f509cf1) to head (e641814).
⚠️ Report is 140 commits behind head on main.

Files with missing linesPatch %Lines
src/crypto/crypto_client_hello.cc44.57%43 Missing and 3 partials ⚠️
Additional details and impacted files
@@ Coverage Diff @@## main #65522 +/- ##
==========================================
- Coverage 90.14% 90.11% -0.03% 
==========================================
Files 751 753 +2 Lines 253585 253733 +148 Branches 47772 47818 +46 ==========================================
+ Hits 228596 228661 +65 - Misses 16228 16308 +80 - Partials 8761 8764 +3 
Files with missing linesCoverage Δ
lib/internal/quic/quic.js100.00% <100.00%> (ø)
src/crypto/crypto_client_hello.h100.00% <100.00%> (ø)
src/crypto/crypto_context.cc71.59% <ø> (-0.07%)⬇️
src/crypto/crypto_context.h100.00% <ø> (ø)
src/crypto/crypto_tls.cc78.78% <100.00%> (+0.01%)⬆️
src/crypto/crypto_client_hello.cc44.57% <44.57%> (ø)

... and 40 files with indirect coverage changes

🚀 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.

@jasnell

Copy link
Copy Markdown
Member

Rather than focusing the PR description on the benefits, can you explain what the behavioral / structural differences here are?

Comment threadsrc/crypto/crypto_client_hello.h Outdated
Comment threadsrc/crypto/crypto_client_hello.h Outdated
Comment threadsrc/crypto/crypto_client_hello.h Outdated
Comment threadsrc/crypto/crypto_client_hello.h Outdated
Comment threadsrc/crypto/crypto_tls.cc Outdated
Comment threadsrc/quic/application.cc Outdated
Comment threadsrc/quic/endpoint.cc
Comment threadsrc/quic/session.cc Outdated
@jasnell

Copy link
Copy Markdown
Member

Ok, I had an agent summarize out the actual changes here. Let me step through them. The quoted bits here are from the agents summary:

The session event is now delivered before the TLS handshake proceeds past the ClientHello, rather than after the first packet is fully processed. No stream, datagram, keylog, origin, application, goaway or datagram-status event can precede it — this is now a protocol-level guarantee rather than a replay artifact.

I know this is the key goal here with your eventual dynamic attach goal.. essentially pause here and let the JS side set things up. My goal has been to defer calling out to JS until we absolutely need to, handling as much of the handshake as possible in C++ as we can.

I'm still not entirely sold on dynamic attach yet. We know at this point that the ALPN is either h3 or something else, and we know that h3 maps to Http3Application and something else maps to the default behavior. Why call out to JavaScript to have it select and install the application? That seems entirely unnecessary and not a performance win. But I guess that's an argument for the larger PR HTTP3/QUIC split PR.

Per-SNI-identity alpn lists are now honoured. Previously the default endpoint context's alpn was always used. (behaviour change

+1 ... this has been on my list to revisit for a while.

Keylog lines are emitted in real time rather than replayed, so their interleaving with other events changes.

Seems fine. We'll need to revisit keylog/qlog when all this is done anyway to ensure we're capturing everything correctly.

session.remoteTransportParams is populated at the session event (previously undefined).

+1

Session tickets with absent or malformed application data are now rejected rather than accepted optimistically

This is good. Absent/malformed application data should mean that the ticket is ignored and a renewed ticket is returned. If we just ignore, the client may continue to just keep trying to use the bad ticket.

When no ALPN is configured on the server, the failure mode moves from OnSelectAlpn returning NOACK to the ClientHello callback continuing with no Application.

This one I'm not convinced about. Currently we will fail the handshake if an ALPN that the server does not recognize is presented. With this, we proceed with the handshake and likely fail later. I'd think that's a regression.

A ClientHello that changes server_name across a HelloRetryRequest is now rejected with illegal_parameter. Previously it was OpenSSL's problem.

+1

Malformed server_name extensions now read as absent (may select the wildcard identity) instead of relying on OpenSSL's parse.

Hmm.. not sure about this one. Smells bad.

Alert selection for SNI failure moves from the servername callback to the ClientHello callback.

+1

Endpoint bytesReceived / packetsReceived under-count when JS closes a session inside the session callback.

This is a regression. The stats should be accurate at this point.

Beyond that, I think this PR could definitely have been better split into multiple commits but ok.

@jasnell

Copy link
Copy Markdown
Member

Overall, +1 but there are a few regressions to investigate.

@pimterry

Copy link
Copy Markdown
MemberAuthor

Code updated. I've replied on the inline points above, but the other remaining notes from your larger comment:

When no ALPN is configured on the server, the failure mode moves from OnSelectAlpn returning NOACK to the ClientHello callback continuing with no Application.

This one I'm not convinced about. Currently we will fail the handshake if an ALPN that the server does not recognize is presented. With this, we proceed with the handshake and likely fail later. I'd think that's a regression.

I think there's a real issue here, but your description is a misreading of the summary. The problem being highlighted is only reachable when the server is explicitly configured with no ALPN protocols (alpn: []) and then a client connects. With this PR, actual ALPN mismatches did still fail the handshake, it was just that invalid ALPN config in the server made invalid application decisions.

I've now tighten this up, with a validation to server creation, to just reject that empty ALPN list at source instead so this is unreachable. I think ALPN is now correctly strict everywhere for QUIC.

Malformed server_name extensions now read as absent (may select the wildcard identity) instead of relying on OpenSSL's parse.

Hmm.. not sure about this one. Smells bad.

Yes, that was a bug. Server name handling now distinguishes the three states (valid SNI/no SNI/malformed SNI) and handles them correctly (do SNI/use wildcard SNI/fail handshake).

Endpoint bytesReceived / packetsReceived under-count when JS closes a session inside the session callback.

This is a regression. The stats should be accurate at this point.

Agreed, now fixed.

Comment threadlib/internal/quic/quic.js
@pimterry

Copy link
Copy Markdown
MemberAuthor

I've now added some proper benchmarks, one for pure connection setup (separately for QUIC vs H3) and one for single HTTP request time (separately for 1RTT & 0RTT). Results on my machine say no significant changes:

./node benchmark/compare.js --old ./node-old --new ./node-new --analyze -- quic
[00:18:50|% 100| 2/2 files | 60/60 runs | 4/4 configs]: Done
confidence improvement accuracy (*) (**) (***)
quic/h3-request.js n=500 mode='0rtt' +0.35 % ±1.23% ±1.64% ±2.13%
quic/h3-request.js n=500 mode='1rtt' +0.11 % ±1.04% ±1.39% ±1.80%
quic/handshake.js n=1000 concurrency=1 protocol='raw' -0.31 % ±1.35% ±1.80% ±2.36%
quic/handshake.js n=1000 concurrency=10 protocol='raw' +0.60 % ±0.98% ±1.31% ±1.70%
quic/handshake.js n=1000 concurrency=1 protocol='h3' * +0.97 % ±0.82% ±1.09% ±1.42%
quic/handshake.js n=1000 concurrency=10 protocol='h3' -0.47 % ±1.06% ±1.42% ±1.86%
-1.8% 0% +1.8%
quic/h3-request.js n=500 mode='0rtt' ░░░░░░░░░░|▓▓▓░░░░░░░░░░░░░░ +0.35% quic/h3-request.js n=500 mode='1rtt' ░░░░░░░░░░|░░░░░░░░░░░░ +0.11% quic/handshake.js n=1000 concurrency=1 protocol='raw' ░░░░░░░░░░░░░░░▓▓▓|░░░░░░░░░░░ -0.31% quic/handshake.js n=1000 concurrency=10 protocol='raw' ░░░░|▓▓▓▓▓▓░░░░░░░░░░░ +0.60% quic/handshake.js n=1000 concurrency=1 protocol='h3' |██████████░░░░░░░░░ +0.97% *
quic/handshake.js n=1000 concurrency=10 protocol='h3' ░░░░░░░░░░░░▓▓▓▓▓|░░░░░░ -0.47% Rates were scaled by 1000x into HdrHistogram (3 significant figures).
Use --scale to adjust precision if needed.

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@jasnell

Copy link
Copy Markdown
Member

oh that new --analyze is so nice ;-) ...

@pimterrypimterry added the commit-queue-squash PRs the Commit Queue should land as one squashed commit. label Sep 2, 2026
@pimterry

Copy link
Copy Markdown
MemberAuthor

@nodejs/quic @nodejs/crypto I think this is good to go now, just needs a final approve covering the docs & benchmark commits.

@panvapanva added author ready PRs with CI started, the required approvals, and no outstanding review comments. commit-queue PRs queued for automated landing through the Commit Queue. labels Sep 2, 2026
@nodejs-github-bot
nodejs-github-bot merged commit 09f5aae into nodejs:mainSep 2, 2026
77 checks passed
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Landed in 09f5aae

@nodejs-github-botnodejs-github-bot removed the commit-queue PRs queued for automated landing through the Commit Queue. label Sep 2, 2026
aduh95 pushed a commit that referenced this pull request Sep 3, 2026
With this change, the TLS pause mechanisms used in node:tls are now made
generic, and used in QUIC to pause the handshake entirely for the
session event, thereby stopping almost all events being emitted
(everything except qlog) so that all the previously existing deferral
machinery is unnecessary.
This is useful immediately to clean up & simplify things, but also
medium term because this makes the 0RTT flow significantly clearer,
which will help with the upcoming dynamic attach for QUIC (which
requires deferring ticket processing & 0RTT delivery until after
user code).
As part of this, this changes the internal flow of how ALPN & SNI are
resolved. Instead of calculating them synchronously within the OpenSSL
callbacks, we precalculate them at the pause, and then the OpenSSL
callbacks just return the precalculated values.
This is intended to only create visible changes to node:quic. Though it
touches node:tls, it preserves all existing behaviour (including
callback order and other details).
Signed-off-by: Tim Perry <pimterry@gmail.com>
PR-URL: #65522
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Filip Skokan <panva.ip@gmail.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

author readyPRs with CI started, the required approvals, and no outstanding review comments.c++Issues and PRs that require attention from people who are familiar with C++.commit-queue-squashPRs the Commit Queue should land as one squashed commit.lib / srcIssues and PRs involving general changes in the lib/ or src/ directories.needs-ciPRs that need a full CI run.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@pimterry@nodejs-github-bot@jasnell@panva
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

quic: reuse TLS pause machinery to drop event deferral & improve 0RTT - #65522

Merged
nodejs-github-bot merged 5 commits into
nodejs:mainfrom
pimterry:pause-for-quic-0rtt
Sep 2, 2026
Merged

quic: reuse TLS pause machinery to drop event deferral & improve 0RTT#65522
nodejs-github-bot merged 5 commits into
nodejs:mainfrom
pimterry:pause-for-quic-0rtt

Conversation

@pimterry

Copy link
Copy Markdown
Member

With this change, the TLS suspend mechanism I introduced in #64827 becomes generic and we reuse it in QUIC. This adds new client hello structures, which hide the OpenSSL vs BoringSSL differences, and expose an interface for each handshake flow to continue, suspend or fail the handshake. While suspended they can access the details and validate/prepare according to their needs.

There's one immediate advantage here: this allows QUIC to reuse this existing mechanism to largely replace its event deferral system. In QUIC, we need to defer some handshake events (mostly relating to resumption & 0RTT, see #64132) until the handshake is sufficiently complete so that at the session event we can do some setup with the data already available, but before the actual data processing within starts.

To do this currently we have a whole event deferral mechanism and multi-step session ticket extraction & validation flow. Directly suspending the handshake reusing the existing mechanism from node:tls lets us delete that completely, do the simple thing instead, and get the same result.

This then has another medium-term benefit: as part of #63995 we will be adding dynamic attach, where the application protocol is decided dynamically on session creation, which requires deferring session ticket processing even further (because we don't even know which protocol to validate for). This mechanism gives us the structure in which to easily do that, giving us 0RTT + dynamic attach.

As part of this, this changes the internal flow of how ALPN & SNI are resolved for QUIC, and extracts ALPN helpers to share more logic between TLS/QUIC/DTLS. Instead of calculating them synchronously within the OpenSSL callbacks, we now precalculate them at the suspend point, and then the OpenSSL callbacks later just use the precalculated values.

This is intended to only create visible changes to node:quic. Though it touches node:tls & node:dtls, it's just commonizing logic and preserves all existing behaviour (including callback order and other details). In future I'd like to make some small breaking changes to both to bring these further inline, but that's tricky and we can ignore it for now.

With this change, the TLS pause mechanisms used in node:tls are now made
generic, and used in QUIC to pause the handshake entirely for the
session event, thereby stopping almost all events being emitted
(everything except qlog) so that all the previously existing deferral
machinery is unnecessary.
This is useful immediately to clean up & simplify things, but also
medium term because this makes the 0RTT flow significantly clearer,
which will help with the upcoming dynamic attach for QUIC (which
requires deferring ticket processing & 0RTT delivery until after
user code).
As part of this, this changes the internal flow of how ALPN & SNI are
resolved. Instead of calculating them synchronously within the OpenSSL
callbacks, we precalculate them at the pause, and then the OpenSSL
callbacks just return the precalculated values.
This is intended to only create visible changes to node:quic. Though it
touches node:tls, it preserves all existing behaviour (including
callback order and other details).
Signed-off-by: Tim Perry <pimterry@gmail.com>
@pimterry
pimterry requested a review from jasnellAugust 24, 2026 18:54
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Review requested:

  • @nodejs/crypto
  • @nodejs/gyp
  • @nodejs/quic

@nodejs-github-botnodejs-github-bot added c++ Issues and PRs that require attention from people who are familiar with C++. lib / src Issues and PRs involving general changes in the lib/ or src/ directories. needs-ci PRs that need a full CI run. labels Aug 24, 2026
@codecov

codecovBot commented Aug 24, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 60.00000% with 46 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.11%. Comparing base (f509cf1) to head (e641814).
⚠️ Report is 140 commits behind head on main.

Files with missing linesPatch %Lines
src/crypto/crypto_client_hello.cc44.57%43 Missing and 3 partials ⚠️
Additional details and impacted files
@@ Coverage Diff @@## main #65522 +/- ##
==========================================
- Coverage 90.14% 90.11% -0.03% 
==========================================
Files 751 753 +2 Lines 253585 253733 +148 Branches 47772 47818 +46 ==========================================
+ Hits 228596 228661 +65 - Misses 16228 16308 +80 - Partials 8761 8764 +3 
Files with missing linesCoverage Δ
lib/internal/quic/quic.js100.00% <100.00%> (ø)
src/crypto/crypto_client_hello.h100.00% <100.00%> (ø)
src/crypto/crypto_context.cc71.59% <ø> (-0.07%)⬇️
src/crypto/crypto_context.h100.00% <ø> (ø)
src/crypto/crypto_tls.cc78.78% <100.00%> (+0.01%)⬆️
src/crypto/crypto_client_hello.cc44.57% <44.57%> (ø)

... and 40 files with indirect coverage changes

🚀 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.

@jasnell

Copy link
Copy Markdown
Member

Rather than focusing the PR description on the benefits, can you explain what the behavioral / structural differences here are?

Comment threadsrc/crypto/crypto_client_hello.h Outdated
Comment threadsrc/crypto/crypto_client_hello.h Outdated
Comment threadsrc/crypto/crypto_client_hello.h Outdated
Comment threadsrc/crypto/crypto_client_hello.h Outdated
Comment threadsrc/crypto/crypto_tls.cc Outdated
Comment threadsrc/quic/application.cc Outdated
Comment threadsrc/quic/endpoint.cc
Comment threadsrc/quic/session.cc Outdated
@jasnell

Copy link
Copy Markdown
Member

Ok, I had an agent summarize out the actual changes here. Let me step through them. The quoted bits here are from the agents summary:

The session event is now delivered before the TLS handshake proceeds past the ClientHello, rather than after the first packet is fully processed. No stream, datagram, keylog, origin, application, goaway or datagram-status event can precede it — this is now a protocol-level guarantee rather than a replay artifact.

I know this is the key goal here with your eventual dynamic attach goal.. essentially pause here and let the JS side set things up. My goal has been to defer calling out to JS until we absolutely need to, handling as much of the handshake as possible in C++ as we can.

I'm still not entirely sold on dynamic attach yet. We know at this point that the ALPN is either h3 or something else, and we know that h3 maps to Http3Application and something else maps to the default behavior. Why call out to JavaScript to have it select and install the application? That seems entirely unnecessary and not a performance win. But I guess that's an argument for the larger PR HTTP3/QUIC split PR.

Per-SNI-identity alpn lists are now honoured. Previously the default endpoint context's alpn was always used. (behaviour change

+1 ... this has been on my list to revisit for a while.

Keylog lines are emitted in real time rather than replayed, so their interleaving with other events changes.

Seems fine. We'll need to revisit keylog/qlog when all this is done anyway to ensure we're capturing everything correctly.

session.remoteTransportParams is populated at the session event (previously undefined).

+1

Session tickets with absent or malformed application data are now rejected rather than accepted optimistically

This is good. Absent/malformed application data should mean that the ticket is ignored and a renewed ticket is returned. If we just ignore, the client may continue to just keep trying to use the bad ticket.

When no ALPN is configured on the server, the failure mode moves from OnSelectAlpn returning NOACK to the ClientHello callback continuing with no Application.

This one I'm not convinced about. Currently we will fail the handshake if an ALPN that the server does not recognize is presented. With this, we proceed with the handshake and likely fail later. I'd think that's a regression.

A ClientHello that changes server_name across a HelloRetryRequest is now rejected with illegal_parameter. Previously it was OpenSSL's problem.

+1

Malformed server_name extensions now read as absent (may select the wildcard identity) instead of relying on OpenSSL's parse.

Hmm.. not sure about this one. Smells bad.

Alert selection for SNI failure moves from the servername callback to the ClientHello callback.

+1

Endpoint bytesReceived / packetsReceived under-count when JS closes a session inside the session callback.

This is a regression. The stats should be accurate at this point.

Beyond that, I think this PR could definitely have been better split into multiple commits but ok.

@jasnell

Copy link
Copy Markdown
Member

Overall, +1 but there are a few regressions to investigate.

@pimterry

Copy link
Copy Markdown
MemberAuthor

Code updated. I've replied on the inline points above, but the other remaining notes from your larger comment:

When no ALPN is configured on the server, the failure mode moves from OnSelectAlpn returning NOACK to the ClientHello callback continuing with no Application.

This one I'm not convinced about. Currently we will fail the handshake if an ALPN that the server does not recognize is presented. With this, we proceed with the handshake and likely fail later. I'd think that's a regression.

I think there's a real issue here, but your description is a misreading of the summary. The problem being highlighted is only reachable when the server is explicitly configured with no ALPN protocols (alpn: []) and then a client connects. With this PR, actual ALPN mismatches did still fail the handshake, it was just that invalid ALPN config in the server made invalid application decisions.

I've now tighten this up, with a validation to server creation, to just reject that empty ALPN list at source instead so this is unreachable. I think ALPN is now correctly strict everywhere for QUIC.

Malformed server_name extensions now read as absent (may select the wildcard identity) instead of relying on OpenSSL's parse.

Hmm.. not sure about this one. Smells bad.

Yes, that was a bug. Server name handling now distinguishes the three states (valid SNI/no SNI/malformed SNI) and handles them correctly (do SNI/use wildcard SNI/fail handshake).

Endpoint bytesReceived / packetsReceived under-count when JS closes a session inside the session callback.

This is a regression. The stats should be accurate at this point.

Agreed, now fixed.

Comment threadlib/internal/quic/quic.js
@pimterry

Copy link
Copy Markdown
MemberAuthor

I've now added some proper benchmarks, one for pure connection setup (separately for QUIC vs H3) and one for single HTTP request time (separately for 1RTT & 0RTT). Results on my machine say no significant changes:

./node benchmark/compare.js --old ./node-old --new ./node-new --analyze -- quic
[00:18:50|% 100| 2/2 files | 60/60 runs | 4/4 configs]: Done
confidence improvement accuracy (*) (**) (***)
quic/h3-request.js n=500 mode='0rtt' +0.35 % ±1.23% ±1.64% ±2.13%
quic/h3-request.js n=500 mode='1rtt' +0.11 % ±1.04% ±1.39% ±1.80%
quic/handshake.js n=1000 concurrency=1 protocol='raw' -0.31 % ±1.35% ±1.80% ±2.36%
quic/handshake.js n=1000 concurrency=10 protocol='raw' +0.60 % ±0.98% ±1.31% ±1.70%
quic/handshake.js n=1000 concurrency=1 protocol='h3' * +0.97 % ±0.82% ±1.09% ±1.42%
quic/handshake.js n=1000 concurrency=10 protocol='h3' -0.47 % ±1.06% ±1.42% ±1.86%
-1.8% 0% +1.8%
quic/h3-request.js n=500 mode='0rtt' ░░░░░░░░░░|▓▓▓░░░░░░░░░░░░░░ +0.35% quic/h3-request.js n=500 mode='1rtt' ░░░░░░░░░░|░░░░░░░░░░░░ +0.11% quic/handshake.js n=1000 concurrency=1 protocol='raw' ░░░░░░░░░░░░░░░▓▓▓|░░░░░░░░░░░ -0.31% quic/handshake.js n=1000 concurrency=10 protocol='raw' ░░░░|▓▓▓▓▓▓░░░░░░░░░░░ +0.60% quic/handshake.js n=1000 concurrency=1 protocol='h3' |██████████░░░░░░░░░ +0.97% *
quic/handshake.js n=1000 concurrency=10 protocol='h3' ░░░░░░░░░░░░▓▓▓▓▓|░░░░░░ -0.47% Rates were scaled by 1000x into HdrHistogram (3 significant figures).
Use --scale to adjust precision if needed.

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@jasnell

Copy link
Copy Markdown
Member

oh that new --analyze is so nice ;-) ...

@pimterrypimterry added the commit-queue-squash PRs the Commit Queue should land as one squashed commit. label Sep 2, 2026
@pimterry

Copy link
Copy Markdown
MemberAuthor

@nodejs/quic @nodejs/crypto I think this is good to go now, just needs a final approve covering the docs & benchmark commits.

@panvapanva added author ready PRs with CI started, the required approvals, and no outstanding review comments. commit-queue PRs queued for automated landing through the Commit Queue. labels Sep 2, 2026
@nodejs-github-bot
nodejs-github-bot merged commit 09f5aae into nodejs:mainSep 2, 2026
77 checks passed
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Landed in 09f5aae

@nodejs-github-botnodejs-github-bot removed the commit-queue PRs queued for automated landing through the Commit Queue. label Sep 2, 2026
aduh95 pushed a commit that referenced this pull request Sep 3, 2026
With this change, the TLS pause mechanisms used in node:tls are now made
generic, and used in QUIC to pause the handshake entirely for the
session event, thereby stopping almost all events being emitted
(everything except qlog) so that all the previously existing deferral
machinery is unnecessary.
This is useful immediately to clean up & simplify things, but also
medium term because this makes the 0RTT flow significantly clearer,
which will help with the upcoming dynamic attach for QUIC (which
requires deferring ticket processing & 0RTT delivery until after
user code).
As part of this, this changes the internal flow of how ALPN & SNI are
resolved. Instead of calculating them synchronously within the OpenSSL
callbacks, we precalculate them at the pause, and then the OpenSSL
callbacks just return the precalculated values.
This is intended to only create visible changes to node:quic. Though it
touches node:tls, it preserves all existing behaviour (including
callback order and other details).
Signed-off-by: Tim Perry <pimterry@gmail.com>
PR-URL: #65522
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Filip Skokan <panva.ip@gmail.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

author readyPRs with CI started, the required approvals, and no outstanding review comments.c++Issues and PRs that require attention from people who are familiar with C++.commit-queue-squashPRs the Commit Queue should land as one squashed commit.lib / srcIssues and PRs involving general changes in the lib/ or src/ directories.needs-ciPRs that need a full CI run.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@pimterry@nodejs-github-bot@jasnell@panva
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

quic: reuse TLS pause machinery to drop event deferral & improve 0RTT - #65522

Merged
nodejs-github-bot merged 5 commits into
nodejs:mainfrom
pimterry:pause-for-quic-0rtt
Sep 2, 2026
Merged

quic: reuse TLS pause machinery to drop event deferral & improve 0RTT#65522
nodejs-github-bot merged 5 commits into
nodejs:mainfrom
pimterry:pause-for-quic-0rtt

Conversation

@pimterry

Copy link
Copy Markdown
Member

With this change, the TLS suspend mechanism I introduced in #64827 becomes generic and we reuse it in QUIC. This adds new client hello structures, which hide the OpenSSL vs BoringSSL differences, and expose an interface for each handshake flow to continue, suspend or fail the handshake. While suspended they can access the details and validate/prepare according to their needs.

There's one immediate advantage here: this allows QUIC to reuse this existing mechanism to largely replace its event deferral system. In QUIC, we need to defer some handshake events (mostly relating to resumption & 0RTT, see #64132) until the handshake is sufficiently complete so that at the session event we can do some setup with the data already available, but before the actual data processing within starts.

To do this currently we have a whole event deferral mechanism and multi-step session ticket extraction & validation flow. Directly suspending the handshake reusing the existing mechanism from node:tls lets us delete that completely, do the simple thing instead, and get the same result.

This then has another medium-term benefit: as part of #63995 we will be adding dynamic attach, where the application protocol is decided dynamically on session creation, which requires deferring session ticket processing even further (because we don't even know which protocol to validate for). This mechanism gives us the structure in which to easily do that, giving us 0RTT + dynamic attach.

As part of this, this changes the internal flow of how ALPN & SNI are resolved for QUIC, and extracts ALPN helpers to share more logic between TLS/QUIC/DTLS. Instead of calculating them synchronously within the OpenSSL callbacks, we now precalculate them at the suspend point, and then the OpenSSL callbacks later just use the precalculated values.

This is intended to only create visible changes to node:quic. Though it touches node:tls & node:dtls, it's just commonizing logic and preserves all existing behaviour (including callback order and other details). In future I'd like to make some small breaking changes to both to bring these further inline, but that's tricky and we can ignore it for now.

With this change, the TLS pause mechanisms used in node:tls are now made
generic, and used in QUIC to pause the handshake entirely for the
session event, thereby stopping almost all events being emitted
(everything except qlog) so that all the previously existing deferral
machinery is unnecessary.
This is useful immediately to clean up & simplify things, but also
medium term because this makes the 0RTT flow significantly clearer,
which will help with the upcoming dynamic attach for QUIC (which
requires deferring ticket processing & 0RTT delivery until after
user code).
As part of this, this changes the internal flow of how ALPN & SNI are
resolved. Instead of calculating them synchronously within the OpenSSL
callbacks, we precalculate them at the pause, and then the OpenSSL
callbacks just return the precalculated values.
This is intended to only create visible changes to node:quic. Though it
touches node:tls, it preserves all existing behaviour (including
callback order and other details).
Signed-off-by: Tim Perry <pimterry@gmail.com>
@pimterry
pimterry requested a review from jasnellAugust 24, 2026 18:54
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Review requested:

  • @nodejs/crypto
  • @nodejs/gyp
  • @nodejs/quic

@nodejs-github-botnodejs-github-bot added c++ Issues and PRs that require attention from people who are familiar with C++. lib / src Issues and PRs involving general changes in the lib/ or src/ directories. needs-ci PRs that need a full CI run. labels Aug 24, 2026
@codecov

codecovBot commented Aug 24, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 60.00000% with 46 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.11%. Comparing base (f509cf1) to head (e641814).
⚠️ Report is 140 commits behind head on main.

Files with missing linesPatch %Lines
src/crypto/crypto_client_hello.cc44.57%43 Missing and 3 partials ⚠️
Additional details and impacted files
@@ Coverage Diff @@## main #65522 +/- ##
==========================================
- Coverage 90.14% 90.11% -0.03% 
==========================================
Files 751 753 +2 Lines 253585 253733 +148 Branches 47772 47818 +46 ==========================================
+ Hits 228596 228661 +65 - Misses 16228 16308 +80 - Partials 8761 8764 +3 
Files with missing linesCoverage Δ
lib/internal/quic/quic.js100.00% <100.00%> (ø)
src/crypto/crypto_client_hello.h100.00% <100.00%> (ø)
src/crypto/crypto_context.cc71.59% <ø> (-0.07%)⬇️
src/crypto/crypto_context.h100.00% <ø> (ø)
src/crypto/crypto_tls.cc78.78% <100.00%> (+0.01%)⬆️
src/crypto/crypto_client_hello.cc44.57% <44.57%> (ø)

... and 40 files with indirect coverage changes

🚀 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.

@jasnell

Copy link
Copy Markdown
Member

Rather than focusing the PR description on the benefits, can you explain what the behavioral / structural differences here are?

Comment threadsrc/crypto/crypto_client_hello.h Outdated
Comment threadsrc/crypto/crypto_client_hello.h Outdated
Comment threadsrc/crypto/crypto_client_hello.h Outdated
Comment threadsrc/crypto/crypto_client_hello.h Outdated
Comment threadsrc/crypto/crypto_tls.cc Outdated
Comment threadsrc/quic/application.cc Outdated
Comment threadsrc/quic/endpoint.cc
Comment threadsrc/quic/session.cc Outdated
@jasnell

Copy link
Copy Markdown
Member

Ok, I had an agent summarize out the actual changes here. Let me step through them. The quoted bits here are from the agents summary:

The session event is now delivered before the TLS handshake proceeds past the ClientHello, rather than after the first packet is fully processed. No stream, datagram, keylog, origin, application, goaway or datagram-status event can precede it — this is now a protocol-level guarantee rather than a replay artifact.

I know this is the key goal here with your eventual dynamic attach goal.. essentially pause here and let the JS side set things up. My goal has been to defer calling out to JS until we absolutely need to, handling as much of the handshake as possible in C++ as we can.

I'm still not entirely sold on dynamic attach yet. We know at this point that the ALPN is either h3 or something else, and we know that h3 maps to Http3Application and something else maps to the default behavior. Why call out to JavaScript to have it select and install the application? That seems entirely unnecessary and not a performance win. But I guess that's an argument for the larger PR HTTP3/QUIC split PR.

Per-SNI-identity alpn lists are now honoured. Previously the default endpoint context's alpn was always used. (behaviour change

+1 ... this has been on my list to revisit for a while.

Keylog lines are emitted in real time rather than replayed, so their interleaving with other events changes.

Seems fine. We'll need to revisit keylog/qlog when all this is done anyway to ensure we're capturing everything correctly.

session.remoteTransportParams is populated at the session event (previously undefined).

+1

Session tickets with absent or malformed application data are now rejected rather than accepted optimistically

This is good. Absent/malformed application data should mean that the ticket is ignored and a renewed ticket is returned. If we just ignore, the client may continue to just keep trying to use the bad ticket.

When no ALPN is configured on the server, the failure mode moves from OnSelectAlpn returning NOACK to the ClientHello callback continuing with no Application.

This one I'm not convinced about. Currently we will fail the handshake if an ALPN that the server does not recognize is presented. With this, we proceed with the handshake and likely fail later. I'd think that's a regression.

A ClientHello that changes server_name across a HelloRetryRequest is now rejected with illegal_parameter. Previously it was OpenSSL's problem.

+1

Malformed server_name extensions now read as absent (may select the wildcard identity) instead of relying on OpenSSL's parse.

Hmm.. not sure about this one. Smells bad.

Alert selection for SNI failure moves from the servername callback to the ClientHello callback.

+1

Endpoint bytesReceived / packetsReceived under-count when JS closes a session inside the session callback.

This is a regression. The stats should be accurate at this point.

Beyond that, I think this PR could definitely have been better split into multiple commits but ok.

@jasnell

Copy link
Copy Markdown
Member

Overall, +1 but there are a few regressions to investigate.

@pimterry

Copy link
Copy Markdown
MemberAuthor

Code updated. I've replied on the inline points above, but the other remaining notes from your larger comment:

When no ALPN is configured on the server, the failure mode moves from OnSelectAlpn returning NOACK to the ClientHello callback continuing with no Application.

This one I'm not convinced about. Currently we will fail the handshake if an ALPN that the server does not recognize is presented. With this, we proceed with the handshake and likely fail later. I'd think that's a regression.

I think there's a real issue here, but your description is a misreading of the summary. The problem being highlighted is only reachable when the server is explicitly configured with no ALPN protocols (alpn: []) and then a client connects. With this PR, actual ALPN mismatches did still fail the handshake, it was just that invalid ALPN config in the server made invalid application decisions.

I've now tighten this up, with a validation to server creation, to just reject that empty ALPN list at source instead so this is unreachable. I think ALPN is now correctly strict everywhere for QUIC.

Malformed server_name extensions now read as absent (may select the wildcard identity) instead of relying on OpenSSL's parse.

Hmm.. not sure about this one. Smells bad.

Yes, that was a bug. Server name handling now distinguishes the three states (valid SNI/no SNI/malformed SNI) and handles them correctly (do SNI/use wildcard SNI/fail handshake).

Endpoint bytesReceived / packetsReceived under-count when JS closes a session inside the session callback.

This is a regression. The stats should be accurate at this point.

Agreed, now fixed.

Comment threadlib/internal/quic/quic.js
@pimterry

Copy link
Copy Markdown
MemberAuthor

I've now added some proper benchmarks, one for pure connection setup (separately for QUIC vs H3) and one for single HTTP request time (separately for 1RTT & 0RTT). Results on my machine say no significant changes:

./node benchmark/compare.js --old ./node-old --new ./node-new --analyze -- quic
[00:18:50|% 100| 2/2 files | 60/60 runs | 4/4 configs]: Done
confidence improvement accuracy (*) (**) (***)
quic/h3-request.js n=500 mode='0rtt' +0.35 % ±1.23% ±1.64% ±2.13%
quic/h3-request.js n=500 mode='1rtt' +0.11 % ±1.04% ±1.39% ±1.80%
quic/handshake.js n=1000 concurrency=1 protocol='raw' -0.31 % ±1.35% ±1.80% ±2.36%
quic/handshake.js n=1000 concurrency=10 protocol='raw' +0.60 % ±0.98% ±1.31% ±1.70%
quic/handshake.js n=1000 concurrency=1 protocol='h3' * +0.97 % ±0.82% ±1.09% ±1.42%
quic/handshake.js n=1000 concurrency=10 protocol='h3' -0.47 % ±1.06% ±1.42% ±1.86%
-1.8% 0% +1.8%
quic/h3-request.js n=500 mode='0rtt' ░░░░░░░░░░|▓▓▓░░░░░░░░░░░░░░ +0.35% quic/h3-request.js n=500 mode='1rtt' ░░░░░░░░░░|░░░░░░░░░░░░ +0.11% quic/handshake.js n=1000 concurrency=1 protocol='raw' ░░░░░░░░░░░░░░░▓▓▓|░░░░░░░░░░░ -0.31% quic/handshake.js n=1000 concurrency=10 protocol='raw' ░░░░|▓▓▓▓▓▓░░░░░░░░░░░ +0.60% quic/handshake.js n=1000 concurrency=1 protocol='h3' |██████████░░░░░░░░░ +0.97% *
quic/handshake.js n=1000 concurrency=10 protocol='h3' ░░░░░░░░░░░░▓▓▓▓▓|░░░░░░ -0.47% Rates were scaled by 1000x into HdrHistogram (3 significant figures).
Use --scale to adjust precision if needed.

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@jasnell

Copy link
Copy Markdown
Member

oh that new --analyze is so nice ;-) ...

@pimterrypimterry added the commit-queue-squash PRs the Commit Queue should land as one squashed commit. label Sep 2, 2026
@pimterry

Copy link
Copy Markdown
MemberAuthor

@nodejs/quic @nodejs/crypto I think this is good to go now, just needs a final approve covering the docs & benchmark commits.

@panvapanva added author ready PRs with CI started, the required approvals, and no outstanding review comments. commit-queue PRs queued for automated landing through the Commit Queue. labels Sep 2, 2026
@nodejs-github-bot
nodejs-github-bot merged commit 09f5aae into nodejs:mainSep 2, 2026
77 checks passed
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Landed in 09f5aae

@nodejs-github-botnodejs-github-bot removed the commit-queue PRs queued for automated landing through the Commit Queue. label Sep 2, 2026
aduh95 pushed a commit that referenced this pull request Sep 3, 2026
With this change, the TLS pause mechanisms used in node:tls are now made
generic, and used in QUIC to pause the handshake entirely for the
session event, thereby stopping almost all events being emitted
(everything except qlog) so that all the previously existing deferral
machinery is unnecessary.
This is useful immediately to clean up & simplify things, but also
medium term because this makes the 0RTT flow significantly clearer,
which will help with the upcoming dynamic attach for QUIC (which
requires deferring ticket processing & 0RTT delivery until after
user code).
As part of this, this changes the internal flow of how ALPN & SNI are
resolved. Instead of calculating them synchronously within the OpenSSL
callbacks, we precalculate them at the pause, and then the OpenSSL
callbacks just return the precalculated values.
This is intended to only create visible changes to node:quic. Though it
touches node:tls, it preserves all existing behaviour (including
callback order and other details).
Signed-off-by: Tim Perry <pimterry@gmail.com>
PR-URL: #65522
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Filip Skokan <panva.ip@gmail.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

author readyPRs with CI started, the required approvals, and no outstanding review comments.c++Issues and PRs that require attention from people who are familiar with C++.commit-queue-squashPRs the Commit Queue should land as one squashed commit.lib / srcIssues and PRs involving general changes in the lib/ or src/ directories.needs-ciPRs that need a full CI run.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@pimterry@nodejs-github-bot@jasnell@panva
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

quic: reuse TLS pause machinery to drop event deferral & improve 0RTT - #65522

Merged
nodejs-github-bot merged 5 commits into
nodejs:mainfrom
pimterry:pause-for-quic-0rtt
Sep 2, 2026
Merged

quic: reuse TLS pause machinery to drop event deferral & improve 0RTT#65522
nodejs-github-bot merged 5 commits into
nodejs:mainfrom
pimterry:pause-for-quic-0rtt

Conversation

@pimterry

Copy link
Copy Markdown
Member

With this change, the TLS suspend mechanism I introduced in #64827 becomes generic and we reuse it in QUIC. This adds new client hello structures, which hide the OpenSSL vs BoringSSL differences, and expose an interface for each handshake flow to continue, suspend or fail the handshake. While suspended they can access the details and validate/prepare according to their needs.

There's one immediate advantage here: this allows QUIC to reuse this existing mechanism to largely replace its event deferral system. In QUIC, we need to defer some handshake events (mostly relating to resumption & 0RTT, see #64132) until the handshake is sufficiently complete so that at the session event we can do some setup with the data already available, but before the actual data processing within starts.

To do this currently we have a whole event deferral mechanism and multi-step session ticket extraction & validation flow. Directly suspending the handshake reusing the existing mechanism from node:tls lets us delete that completely, do the simple thing instead, and get the same result.

This then has another medium-term benefit: as part of #63995 we will be adding dynamic attach, where the application protocol is decided dynamically on session creation, which requires deferring session ticket processing even further (because we don't even know which protocol to validate for). This mechanism gives us the structure in which to easily do that, giving us 0RTT + dynamic attach.

As part of this, this changes the internal flow of how ALPN & SNI are resolved for QUIC, and extracts ALPN helpers to share more logic between TLS/QUIC/DTLS. Instead of calculating them synchronously within the OpenSSL callbacks, we now precalculate them at the suspend point, and then the OpenSSL callbacks later just use the precalculated values.

This is intended to only create visible changes to node:quic. Though it touches node:tls & node:dtls, it's just commonizing logic and preserves all existing behaviour (including callback order and other details). In future I'd like to make some small breaking changes to both to bring these further inline, but that's tricky and we can ignore it for now.

With this change, the TLS pause mechanisms used in node:tls are now made
generic, and used in QUIC to pause the handshake entirely for the
session event, thereby stopping almost all events being emitted
(everything except qlog) so that all the previously existing deferral
machinery is unnecessary.
This is useful immediately to clean up & simplify things, but also
medium term because this makes the 0RTT flow significantly clearer,
which will help with the upcoming dynamic attach for QUIC (which
requires deferring ticket processing & 0RTT delivery until after
user code).
As part of this, this changes the internal flow of how ALPN & SNI are
resolved. Instead of calculating them synchronously within the OpenSSL
callbacks, we precalculate them at the pause, and then the OpenSSL
callbacks just return the precalculated values.
This is intended to only create visible changes to node:quic. Though it
touches node:tls, it preserves all existing behaviour (including
callback order and other details).
Signed-off-by: Tim Perry <pimterry@gmail.com>
@pimterry
pimterry requested a review from jasnellAugust 24, 2026 18:54
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Review requested:

  • @nodejs/crypto
  • @nodejs/gyp
  • @nodejs/quic

@nodejs-github-botnodejs-github-bot added c++ Issues and PRs that require attention from people who are familiar with C++. lib / src Issues and PRs involving general changes in the lib/ or src/ directories. needs-ci PRs that need a full CI run. labels Aug 24, 2026
@codecov

codecovBot commented Aug 24, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 60.00000% with 46 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.11%. Comparing base (f509cf1) to head (e641814).
⚠️ Report is 140 commits behind head on main.

Files with missing linesPatch %Lines
src/crypto/crypto_client_hello.cc44.57%43 Missing and 3 partials ⚠️
Additional details and impacted files
@@ Coverage Diff @@## main #65522 +/- ##
==========================================
- Coverage 90.14% 90.11% -0.03% 
==========================================
Files 751 753 +2 Lines 253585 253733 +148 Branches 47772 47818 +46 ==========================================
+ Hits 228596 228661 +65 - Misses 16228 16308 +80 - Partials 8761 8764 +3 
Files with missing linesCoverage Δ
lib/internal/quic/quic.js100.00% <100.00%> (ø)
src/crypto/crypto_client_hello.h100.00% <100.00%> (ø)
src/crypto/crypto_context.cc71.59% <ø> (-0.07%)⬇️
src/crypto/crypto_context.h100.00% <ø> (ø)
src/crypto/crypto_tls.cc78.78% <100.00%> (+0.01%)⬆️
src/crypto/crypto_client_hello.cc44.57% <44.57%> (ø)

... and 40 files with indirect coverage changes

🚀 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.

@jasnell

Copy link
Copy Markdown
Member

Rather than focusing the PR description on the benefits, can you explain what the behavioral / structural differences here are?

Comment threadsrc/crypto/crypto_client_hello.h Outdated
Comment threadsrc/crypto/crypto_client_hello.h Outdated
Comment threadsrc/crypto/crypto_client_hello.h Outdated
Comment threadsrc/crypto/crypto_client_hello.h Outdated
Comment threadsrc/crypto/crypto_tls.cc Outdated
Comment threadsrc/quic/application.cc Outdated
Comment threadsrc/quic/endpoint.cc
Comment threadsrc/quic/session.cc Outdated
@jasnell

Copy link
Copy Markdown
Member

Ok, I had an agent summarize out the actual changes here. Let me step through them. The quoted bits here are from the agents summary:

The session event is now delivered before the TLS handshake proceeds past the ClientHello, rather than after the first packet is fully processed. No stream, datagram, keylog, origin, application, goaway or datagram-status event can precede it — this is now a protocol-level guarantee rather than a replay artifact.

I know this is the key goal here with your eventual dynamic attach goal.. essentially pause here and let the JS side set things up. My goal has been to defer calling out to JS until we absolutely need to, handling as much of the handshake as possible in C++ as we can.

I'm still not entirely sold on dynamic attach yet. We know at this point that the ALPN is either h3 or something else, and we know that h3 maps to Http3Application and something else maps to the default behavior. Why call out to JavaScript to have it select and install the application? That seems entirely unnecessary and not a performance win. But I guess that's an argument for the larger PR HTTP3/QUIC split PR.

Per-SNI-identity alpn lists are now honoured. Previously the default endpoint context's alpn was always used. (behaviour change

+1 ... this has been on my list to revisit for a while.

Keylog lines are emitted in real time rather than replayed, so their interleaving with other events changes.

Seems fine. We'll need to revisit keylog/qlog when all this is done anyway to ensure we're capturing everything correctly.

session.remoteTransportParams is populated at the session event (previously undefined).

+1

Session tickets with absent or malformed application data are now rejected rather than accepted optimistically

This is good. Absent/malformed application data should mean that the ticket is ignored and a renewed ticket is returned. If we just ignore, the client may continue to just keep trying to use the bad ticket.

When no ALPN is configured on the server, the failure mode moves from OnSelectAlpn returning NOACK to the ClientHello callback continuing with no Application.

This one I'm not convinced about. Currently we will fail the handshake if an ALPN that the server does not recognize is presented. With this, we proceed with the handshake and likely fail later. I'd think that's a regression.

A ClientHello that changes server_name across a HelloRetryRequest is now rejected with illegal_parameter. Previously it was OpenSSL's problem.

+1

Malformed server_name extensions now read as absent (may select the wildcard identity) instead of relying on OpenSSL's parse.

Hmm.. not sure about this one. Smells bad.

Alert selection for SNI failure moves from the servername callback to the ClientHello callback.

+1

Endpoint bytesReceived / packetsReceived under-count when JS closes a session inside the session callback.

This is a regression. The stats should be accurate at this point.

Beyond that, I think this PR could definitely have been better split into multiple commits but ok.

@jasnell

Copy link
Copy Markdown
Member

Overall, +1 but there are a few regressions to investigate.

@pimterry

Copy link
Copy Markdown
MemberAuthor

Code updated. I've replied on the inline points above, but the other remaining notes from your larger comment:

When no ALPN is configured on the server, the failure mode moves from OnSelectAlpn returning NOACK to the ClientHello callback continuing with no Application.

This one I'm not convinced about. Currently we will fail the handshake if an ALPN that the server does not recognize is presented. With this, we proceed with the handshake and likely fail later. I'd think that's a regression.

I think there's a real issue here, but your description is a misreading of the summary. The problem being highlighted is only reachable when the server is explicitly configured with no ALPN protocols (alpn: []) and then a client connects. With this PR, actual ALPN mismatches did still fail the handshake, it was just that invalid ALPN config in the server made invalid application decisions.

I've now tighten this up, with a validation to server creation, to just reject that empty ALPN list at source instead so this is unreachable. I think ALPN is now correctly strict everywhere for QUIC.

Malformed server_name extensions now read as absent (may select the wildcard identity) instead of relying on OpenSSL's parse.

Hmm.. not sure about this one. Smells bad.

Yes, that was a bug. Server name handling now distinguishes the three states (valid SNI/no SNI/malformed SNI) and handles them correctly (do SNI/use wildcard SNI/fail handshake).

Endpoint bytesReceived / packetsReceived under-count when JS closes a session inside the session callback.

This is a regression. The stats should be accurate at this point.

Agreed, now fixed.

Comment threadlib/internal/quic/quic.js
@pimterry

Copy link
Copy Markdown
MemberAuthor

I've now added some proper benchmarks, one for pure connection setup (separately for QUIC vs H3) and one for single HTTP request time (separately for 1RTT & 0RTT). Results on my machine say no significant changes:

./node benchmark/compare.js --old ./node-old --new ./node-new --analyze -- quic
[00:18:50|% 100| 2/2 files | 60/60 runs | 4/4 configs]: Done
confidence improvement accuracy (*) (**) (***)
quic/h3-request.js n=500 mode='0rtt' +0.35 % ±1.23% ±1.64% ±2.13%
quic/h3-request.js n=500 mode='1rtt' +0.11 % ±1.04% ±1.39% ±1.80%
quic/handshake.js n=1000 concurrency=1 protocol='raw' -0.31 % ±1.35% ±1.80% ±2.36%
quic/handshake.js n=1000 concurrency=10 protocol='raw' +0.60 % ±0.98% ±1.31% ±1.70%
quic/handshake.js n=1000 concurrency=1 protocol='h3' * +0.97 % ±0.82% ±1.09% ±1.42%
quic/handshake.js n=1000 concurrency=10 protocol='h3' -0.47 % ±1.06% ±1.42% ±1.86%
-1.8% 0% +1.8%
quic/h3-request.js n=500 mode='0rtt' ░░░░░░░░░░|▓▓▓░░░░░░░░░░░░░░ +0.35% quic/h3-request.js n=500 mode='1rtt' ░░░░░░░░░░|░░░░░░░░░░░░ +0.11% quic/handshake.js n=1000 concurrency=1 protocol='raw' ░░░░░░░░░░░░░░░▓▓▓|░░░░░░░░░░░ -0.31% quic/handshake.js n=1000 concurrency=10 protocol='raw' ░░░░|▓▓▓▓▓▓░░░░░░░░░░░ +0.60% quic/handshake.js n=1000 concurrency=1 protocol='h3' |██████████░░░░░░░░░ +0.97% *
quic/handshake.js n=1000 concurrency=10 protocol='h3' ░░░░░░░░░░░░▓▓▓▓▓|░░░░░░ -0.47% Rates were scaled by 1000x into HdrHistogram (3 significant figures).
Use --scale to adjust precision if needed.

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@jasnell

Copy link
Copy Markdown
Member

oh that new --analyze is so nice ;-) ...

@pimterrypimterry added the commit-queue-squash PRs the Commit Queue should land as one squashed commit. label Sep 2, 2026
@pimterry

Copy link
Copy Markdown
MemberAuthor

@nodejs/quic @nodejs/crypto I think this is good to go now, just needs a final approve covering the docs & benchmark commits.

@panvapanva added author ready PRs with CI started, the required approvals, and no outstanding review comments. commit-queue PRs queued for automated landing through the Commit Queue. labels Sep 2, 2026
@nodejs-github-bot
nodejs-github-bot merged commit 09f5aae into nodejs:mainSep 2, 2026
77 checks passed
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Landed in 09f5aae

@nodejs-github-botnodejs-github-bot removed the commit-queue PRs queued for automated landing through the Commit Queue. label Sep 2, 2026
aduh95 pushed a commit that referenced this pull request Sep 3, 2026
With this change, the TLS pause mechanisms used in node:tls are now made
generic, and used in QUIC to pause the handshake entirely for the
session event, thereby stopping almost all events being emitted
(everything except qlog) so that all the previously existing deferral
machinery is unnecessary.
This is useful immediately to clean up & simplify things, but also
medium term because this makes the 0RTT flow significantly clearer,
which will help with the upcoming dynamic attach for QUIC (which
requires deferring ticket processing & 0RTT delivery until after
user code).
As part of this, this changes the internal flow of how ALPN & SNI are
resolved. Instead of calculating them synchronously within the OpenSSL
callbacks, we precalculate them at the pause, and then the OpenSSL
callbacks just return the precalculated values.
This is intended to only create visible changes to node:quic. Though it
touches node:tls, it preserves all existing behaviour (including
callback order and other details).
Signed-off-by: Tim Perry <pimterry@gmail.com>
PR-URL: #65522
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Filip Skokan <panva.ip@gmail.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

author readyPRs with CI started, the required approvals, and no outstanding review comments.c++Issues and PRs that require attention from people who are familiar with C++.commit-queue-squashPRs the Commit Queue should land as one squashed commit.lib / srcIssues and PRs involving general changes in the lib/ or src/ directories.needs-ciPRs that need a full CI run.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@pimterry@nodejs-github-bot@jasnell@panva
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

quic: reuse TLS pause machinery to drop event deferral & improve 0RTT - #65522

Merged
nodejs-github-bot merged 5 commits into
nodejs:mainfrom
pimterry:pause-for-quic-0rtt
Sep 2, 2026
Merged

quic: reuse TLS pause machinery to drop event deferral & improve 0RTT#65522
nodejs-github-bot merged 5 commits into
nodejs:mainfrom
pimterry:pause-for-quic-0rtt

Conversation

@pimterry

Copy link
Copy Markdown
Member

With this change, the TLS suspend mechanism I introduced in #64827 becomes generic and we reuse it in QUIC. This adds new client hello structures, which hide the OpenSSL vs BoringSSL differences, and expose an interface for each handshake flow to continue, suspend or fail the handshake. While suspended they can access the details and validate/prepare according to their needs.

There's one immediate advantage here: this allows QUIC to reuse this existing mechanism to largely replace its event deferral system. In QUIC, we need to defer some handshake events (mostly relating to resumption & 0RTT, see #64132) until the handshake is sufficiently complete so that at the session event we can do some setup with the data already available, but before the actual data processing within starts.

To do this currently we have a whole event deferral mechanism and multi-step session ticket extraction & validation flow. Directly suspending the handshake reusing the existing mechanism from node:tls lets us delete that completely, do the simple thing instead, and get the same result.

This then has another medium-term benefit: as part of #63995 we will be adding dynamic attach, where the application protocol is decided dynamically on session creation, which requires deferring session ticket processing even further (because we don't even know which protocol to validate for). This mechanism gives us the structure in which to easily do that, giving us 0RTT + dynamic attach.

As part of this, this changes the internal flow of how ALPN & SNI are resolved for QUIC, and extracts ALPN helpers to share more logic between TLS/QUIC/DTLS. Instead of calculating them synchronously within the OpenSSL callbacks, we now precalculate them at the suspend point, and then the OpenSSL callbacks later just use the precalculated values.

This is intended to only create visible changes to node:quic. Though it touches node:tls & node:dtls, it's just commonizing logic and preserves all existing behaviour (including callback order and other details). In future I'd like to make some small breaking changes to both to bring these further inline, but that's tricky and we can ignore it for now.

With this change, the TLS pause mechanisms used in node:tls are now made
generic, and used in QUIC to pause the handshake entirely for the
session event, thereby stopping almost all events being emitted
(everything except qlog) so that all the previously existing deferral
machinery is unnecessary.
This is useful immediately to clean up & simplify things, but also
medium term because this makes the 0RTT flow significantly clearer,
which will help with the upcoming dynamic attach for QUIC (which
requires deferring ticket processing & 0RTT delivery until after
user code).
As part of this, this changes the internal flow of how ALPN & SNI are
resolved. Instead of calculating them synchronously within the OpenSSL
callbacks, we precalculate them at the pause, and then the OpenSSL
callbacks just return the precalculated values.
This is intended to only create visible changes to node:quic. Though it
touches node:tls, it preserves all existing behaviour (including
callback order and other details).
Signed-off-by: Tim Perry <pimterry@gmail.com>
@pimterry
pimterry requested a review from jasnellAugust 24, 2026 18:54
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Review requested:

  • @nodejs/crypto
  • @nodejs/gyp
  • @nodejs/quic

@nodejs-github-botnodejs-github-bot added c++ Issues and PRs that require attention from people who are familiar with C++. lib / src Issues and PRs involving general changes in the lib/ or src/ directories. needs-ci PRs that need a full CI run. labels Aug 24, 2026
@codecov

codecovBot commented Aug 24, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 60.00000% with 46 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.11%. Comparing base (f509cf1) to head (e641814).
⚠️ Report is 140 commits behind head on main.

Files with missing linesPatch %Lines
src/crypto/crypto_client_hello.cc44.57%43 Missing and 3 partials ⚠️
Additional details and impacted files
@@ Coverage Diff @@## main #65522 +/- ##
==========================================
- Coverage 90.14% 90.11% -0.03% 
==========================================
Files 751 753 +2 Lines 253585 253733 +148 Branches 47772 47818 +46 ==========================================
+ Hits 228596 228661 +65 - Misses 16228 16308 +80 - Partials 8761 8764 +3 
Files with missing linesCoverage Δ
lib/internal/quic/quic.js100.00% <100.00%> (ø)
src/crypto/crypto_client_hello.h100.00% <100.00%> (ø)
src/crypto/crypto_context.cc71.59% <ø> (-0.07%)⬇️
src/crypto/crypto_context.h100.00% <ø> (ø)
src/crypto/crypto_tls.cc78.78% <100.00%> (+0.01%)⬆️
src/crypto/crypto_client_hello.cc44.57% <44.57%> (ø)

... and 40 files with indirect coverage changes

🚀 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.

@jasnell

Copy link
Copy Markdown
Member

Rather than focusing the PR description on the benefits, can you explain what the behavioral / structural differences here are?

Comment threadsrc/crypto/crypto_client_hello.h Outdated
Comment threadsrc/crypto/crypto_client_hello.h Outdated
Comment threadsrc/crypto/crypto_client_hello.h Outdated
Comment threadsrc/crypto/crypto_client_hello.h Outdated
Comment threadsrc/crypto/crypto_tls.cc Outdated
Comment threadsrc/quic/application.cc Outdated
Comment threadsrc/quic/endpoint.cc
Comment threadsrc/quic/session.cc Outdated
@jasnell

Copy link
Copy Markdown
Member

Ok, I had an agent summarize out the actual changes here. Let me step through them. The quoted bits here are from the agents summary:

The session event is now delivered before the TLS handshake proceeds past the ClientHello, rather than after the first packet is fully processed. No stream, datagram, keylog, origin, application, goaway or datagram-status event can precede it — this is now a protocol-level guarantee rather than a replay artifact.

I know this is the key goal here with your eventual dynamic attach goal.. essentially pause here and let the JS side set things up. My goal has been to defer calling out to JS until we absolutely need to, handling as much of the handshake as possible in C++ as we can.

I'm still not entirely sold on dynamic attach yet. We know at this point that the ALPN is either h3 or something else, and we know that h3 maps to Http3Application and something else maps to the default behavior. Why call out to JavaScript to have it select and install the application? That seems entirely unnecessary and not a performance win. But I guess that's an argument for the larger PR HTTP3/QUIC split PR.

Per-SNI-identity alpn lists are now honoured. Previously the default endpoint context's alpn was always used. (behaviour change

+1 ... this has been on my list to revisit for a while.

Keylog lines are emitted in real time rather than replayed, so their interleaving with other events changes.

Seems fine. We'll need to revisit keylog/qlog when all this is done anyway to ensure we're capturing everything correctly.

session.remoteTransportParams is populated at the session event (previously undefined).

+1

Session tickets with absent or malformed application data are now rejected rather than accepted optimistically

This is good. Absent/malformed application data should mean that the ticket is ignored and a renewed ticket is returned. If we just ignore, the client may continue to just keep trying to use the bad ticket.

When no ALPN is configured on the server, the failure mode moves from OnSelectAlpn returning NOACK to the ClientHello callback continuing with no Application.

This one I'm not convinced about. Currently we will fail the handshake if an ALPN that the server does not recognize is presented. With this, we proceed with the handshake and likely fail later. I'd think that's a regression.

A ClientHello that changes server_name across a HelloRetryRequest is now rejected with illegal_parameter. Previously it was OpenSSL's problem.

+1

Malformed server_name extensions now read as absent (may select the wildcard identity) instead of relying on OpenSSL's parse.

Hmm.. not sure about this one. Smells bad.

Alert selection for SNI failure moves from the servername callback to the ClientHello callback.

+1

Endpoint bytesReceived / packetsReceived under-count when JS closes a session inside the session callback.

This is a regression. The stats should be accurate at this point.

Beyond that, I think this PR could definitely have been better split into multiple commits but ok.

@jasnell

Copy link
Copy Markdown
Member

Overall, +1 but there are a few regressions to investigate.

@pimterry

Copy link
Copy Markdown
MemberAuthor

Code updated. I've replied on the inline points above, but the other remaining notes from your larger comment:

When no ALPN is configured on the server, the failure mode moves from OnSelectAlpn returning NOACK to the ClientHello callback continuing with no Application.

This one I'm not convinced about. Currently we will fail the handshake if an ALPN that the server does not recognize is presented. With this, we proceed with the handshake and likely fail later. I'd think that's a regression.

I think there's a real issue here, but your description is a misreading of the summary. The problem being highlighted is only reachable when the server is explicitly configured with no ALPN protocols (alpn: []) and then a client connects. With this PR, actual ALPN mismatches did still fail the handshake, it was just that invalid ALPN config in the server made invalid application decisions.

I've now tighten this up, with a validation to server creation, to just reject that empty ALPN list at source instead so this is unreachable. I think ALPN is now correctly strict everywhere for QUIC.

Malformed server_name extensions now read as absent (may select the wildcard identity) instead of relying on OpenSSL's parse.

Hmm.. not sure about this one. Smells bad.

Yes, that was a bug. Server name handling now distinguishes the three states (valid SNI/no SNI/malformed SNI) and handles them correctly (do SNI/use wildcard SNI/fail handshake).

Endpoint bytesReceived / packetsReceived under-count when JS closes a session inside the session callback.

This is a regression. The stats should be accurate at this point.

Agreed, now fixed.

Comment threadlib/internal/quic/quic.js
@pimterry

Copy link
Copy Markdown
MemberAuthor

I've now added some proper benchmarks, one for pure connection setup (separately for QUIC vs H3) and one for single HTTP request time (separately for 1RTT & 0RTT). Results on my machine say no significant changes:

./node benchmark/compare.js --old ./node-old --new ./node-new --analyze -- quic
[00:18:50|% 100| 2/2 files | 60/60 runs | 4/4 configs]: Done
confidence improvement accuracy (*) (**) (***)
quic/h3-request.js n=500 mode='0rtt' +0.35 % ±1.23% ±1.64% ±2.13%
quic/h3-request.js n=500 mode='1rtt' +0.11 % ±1.04% ±1.39% ±1.80%
quic/handshake.js n=1000 concurrency=1 protocol='raw' -0.31 % ±1.35% ±1.80% ±2.36%
quic/handshake.js n=1000 concurrency=10 protocol='raw' +0.60 % ±0.98% ±1.31% ±1.70%
quic/handshake.js n=1000 concurrency=1 protocol='h3' * +0.97 % ±0.82% ±1.09% ±1.42%
quic/handshake.js n=1000 concurrency=10 protocol='h3' -0.47 % ±1.06% ±1.42% ±1.86%
-1.8% 0% +1.8%
quic/h3-request.js n=500 mode='0rtt' ░░░░░░░░░░|▓▓▓░░░░░░░░░░░░░░ +0.35% quic/h3-request.js n=500 mode='1rtt' ░░░░░░░░░░|░░░░░░░░░░░░ +0.11% quic/handshake.js n=1000 concurrency=1 protocol='raw' ░░░░░░░░░░░░░░░▓▓▓|░░░░░░░░░░░ -0.31% quic/handshake.js n=1000 concurrency=10 protocol='raw' ░░░░|▓▓▓▓▓▓░░░░░░░░░░░ +0.60% quic/handshake.js n=1000 concurrency=1 protocol='h3' |██████████░░░░░░░░░ +0.97% *
quic/handshake.js n=1000 concurrency=10 protocol='h3' ░░░░░░░░░░░░▓▓▓▓▓|░░░░░░ -0.47% Rates were scaled by 1000x into HdrHistogram (3 significant figures).
Use --scale to adjust precision if needed.

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@jasnell

Copy link
Copy Markdown
Member

oh that new --analyze is so nice ;-) ...

@pimterrypimterry added the commit-queue-squash PRs the Commit Queue should land as one squashed commit. label Sep 2, 2026
@pimterry

Copy link
Copy Markdown
MemberAuthor

@nodejs/quic @nodejs/crypto I think this is good to go now, just needs a final approve covering the docs & benchmark commits.

@panvapanva added author ready PRs with CI started, the required approvals, and no outstanding review comments. commit-queue PRs queued for automated landing through the Commit Queue. labels Sep 2, 2026
@nodejs-github-bot
nodejs-github-bot merged commit 09f5aae into nodejs:mainSep 2, 2026
77 checks passed
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Landed in 09f5aae

@nodejs-github-botnodejs-github-bot removed the commit-queue PRs queued for automated landing through the Commit Queue. label Sep 2, 2026
aduh95 pushed a commit that referenced this pull request Sep 3, 2026
With this change, the TLS pause mechanisms used in node:tls are now made
generic, and used in QUIC to pause the handshake entirely for the
session event, thereby stopping almost all events being emitted
(everything except qlog) so that all the previously existing deferral
machinery is unnecessary.
This is useful immediately to clean up & simplify things, but also
medium term because this makes the 0RTT flow significantly clearer,
which will help with the upcoming dynamic attach for QUIC (which
requires deferring ticket processing & 0RTT delivery until after
user code).
As part of this, this changes the internal flow of how ALPN & SNI are
resolved. Instead of calculating them synchronously within the OpenSSL
callbacks, we precalculate them at the pause, and then the OpenSSL
callbacks just return the precalculated values.
This is intended to only create visible changes to node:quic. Though it
touches node:tls, it preserves all existing behaviour (including
callback order and other details).
Signed-off-by: Tim Perry <pimterry@gmail.com>
PR-URL: #65522
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Filip Skokan <panva.ip@gmail.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

author readyPRs with CI started, the required approvals, and no outstanding review comments.c++Issues and PRs that require attention from people who are familiar with C++.commit-queue-squashPRs the Commit Queue should land as one squashed commit.lib / srcIssues and PRs involving general changes in the lib/ or src/ directories.needs-ciPRs that need a full CI run.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@pimterry@nodejs-github-bot@jasnell@panva
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

quic: reuse TLS pause machinery to drop event deferral & improve 0RTT - #65522

Merged
nodejs-github-bot merged 5 commits into
nodejs:mainfrom
pimterry:pause-for-quic-0rtt
Sep 2, 2026
Merged

quic: reuse TLS pause machinery to drop event deferral & improve 0RTT#65522
nodejs-github-bot merged 5 commits into
nodejs:mainfrom
pimterry:pause-for-quic-0rtt

Conversation

@pimterry

Copy link
Copy Markdown
Member

With this change, the TLS suspend mechanism I introduced in #64827 becomes generic and we reuse it in QUIC. This adds new client hello structures, which hide the OpenSSL vs BoringSSL differences, and expose an interface for each handshake flow to continue, suspend or fail the handshake. While suspended they can access the details and validate/prepare according to their needs.

There's one immediate advantage here: this allows QUIC to reuse this existing mechanism to largely replace its event deferral system. In QUIC, we need to defer some handshake events (mostly relating to resumption & 0RTT, see #64132) until the handshake is sufficiently complete so that at the session event we can do some setup with the data already available, but before the actual data processing within starts.

To do this currently we have a whole event deferral mechanism and multi-step session ticket extraction & validation flow. Directly suspending the handshake reusing the existing mechanism from node:tls lets us delete that completely, do the simple thing instead, and get the same result.

This then has another medium-term benefit: as part of #63995 we will be adding dynamic attach, where the application protocol is decided dynamically on session creation, which requires deferring session ticket processing even further (because we don't even know which protocol to validate for). This mechanism gives us the structure in which to easily do that, giving us 0RTT + dynamic attach.

As part of this, this changes the internal flow of how ALPN & SNI are resolved for QUIC, and extracts ALPN helpers to share more logic between TLS/QUIC/DTLS. Instead of calculating them synchronously within the OpenSSL callbacks, we now precalculate them at the suspend point, and then the OpenSSL callbacks later just use the precalculated values.

This is intended to only create visible changes to node:quic. Though it touches node:tls & node:dtls, it's just commonizing logic and preserves all existing behaviour (including callback order and other details). In future I'd like to make some small breaking changes to both to bring these further inline, but that's tricky and we can ignore it for now.

With this change, the TLS pause mechanisms used in node:tls are now made
generic, and used in QUIC to pause the handshake entirely for the
session event, thereby stopping almost all events being emitted
(everything except qlog) so that all the previously existing deferral
machinery is unnecessary.
This is useful immediately to clean up & simplify things, but also
medium term because this makes the 0RTT flow significantly clearer,
which will help with the upcoming dynamic attach for QUIC (which
requires deferring ticket processing & 0RTT delivery until after
user code).
As part of this, this changes the internal flow of how ALPN & SNI are
resolved. Instead of calculating them synchronously within the OpenSSL
callbacks, we precalculate them at the pause, and then the OpenSSL
callbacks just return the precalculated values.
This is intended to only create visible changes to node:quic. Though it
touches node:tls, it preserves all existing behaviour (including
callback order and other details).
Signed-off-by: Tim Perry <pimterry@gmail.com>
@pimterry
pimterry requested a review from jasnellAugust 24, 2026 18:54
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Review requested:

  • @nodejs/crypto
  • @nodejs/gyp
  • @nodejs/quic

@nodejs-github-botnodejs-github-bot added c++ Issues and PRs that require attention from people who are familiar with C++. lib / src Issues and PRs involving general changes in the lib/ or src/ directories. needs-ci PRs that need a full CI run. labels Aug 24, 2026
@codecov

codecovBot commented Aug 24, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 60.00000% with 46 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.11%. Comparing base (f509cf1) to head (e641814).
⚠️ Report is 140 commits behind head on main.

Files with missing linesPatch %Lines
src/crypto/crypto_client_hello.cc44.57%43 Missing and 3 partials ⚠️
Additional details and impacted files
@@ Coverage Diff @@## main #65522 +/- ##
==========================================
- Coverage 90.14% 90.11% -0.03% 
==========================================
Files 751 753 +2 Lines 253585 253733 +148 Branches 47772 47818 +46 ==========================================
+ Hits 228596 228661 +65 - Misses 16228 16308 +80 - Partials 8761 8764 +3 
Files with missing linesCoverage Δ
lib/internal/quic/quic.js100.00% <100.00%> (ø)
src/crypto/crypto_client_hello.h100.00% <100.00%> (ø)
src/crypto/crypto_context.cc71.59% <ø> (-0.07%)⬇️
src/crypto/crypto_context.h100.00% <ø> (ø)
src/crypto/crypto_tls.cc78.78% <100.00%> (+0.01%)⬆️
src/crypto/crypto_client_hello.cc44.57% <44.57%> (ø)

... and 40 files with indirect coverage changes

🚀 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.

@jasnell

Copy link
Copy Markdown
Member

Rather than focusing the PR description on the benefits, can you explain what the behavioral / structural differences here are?

Comment threadsrc/crypto/crypto_client_hello.h Outdated
Comment threadsrc/crypto/crypto_client_hello.h Outdated
Comment threadsrc/crypto/crypto_client_hello.h Outdated
Comment threadsrc/crypto/crypto_client_hello.h Outdated
Comment threadsrc/crypto/crypto_tls.cc Outdated
Comment threadsrc/quic/application.cc Outdated
Comment threadsrc/quic/endpoint.cc
Comment threadsrc/quic/session.cc Outdated
@jasnell

Copy link
Copy Markdown
Member

Ok, I had an agent summarize out the actual changes here. Let me step through them. The quoted bits here are from the agents summary:

The session event is now delivered before the TLS handshake proceeds past the ClientHello, rather than after the first packet is fully processed. No stream, datagram, keylog, origin, application, goaway or datagram-status event can precede it — this is now a protocol-level guarantee rather than a replay artifact.

I know this is the key goal here with your eventual dynamic attach goal.. essentially pause here and let the JS side set things up. My goal has been to defer calling out to JS until we absolutely need to, handling as much of the handshake as possible in C++ as we can.

I'm still not entirely sold on dynamic attach yet. We know at this point that the ALPN is either h3 or something else, and we know that h3 maps to Http3Application and something else maps to the default behavior. Why call out to JavaScript to have it select and install the application? That seems entirely unnecessary and not a performance win. But I guess that's an argument for the larger PR HTTP3/QUIC split PR.

Per-SNI-identity alpn lists are now honoured. Previously the default endpoint context's alpn was always used. (behaviour change

+1 ... this has been on my list to revisit for a while.

Keylog lines are emitted in real time rather than replayed, so their interleaving with other events changes.

Seems fine. We'll need to revisit keylog/qlog when all this is done anyway to ensure we're capturing everything correctly.

session.remoteTransportParams is populated at the session event (previously undefined).

+1

Session tickets with absent or malformed application data are now rejected rather than accepted optimistically

This is good. Absent/malformed application data should mean that the ticket is ignored and a renewed ticket is returned. If we just ignore, the client may continue to just keep trying to use the bad ticket.

When no ALPN is configured on the server, the failure mode moves from OnSelectAlpn returning NOACK to the ClientHello callback continuing with no Application.

This one I'm not convinced about. Currently we will fail the handshake if an ALPN that the server does not recognize is presented. With this, we proceed with the handshake and likely fail later. I'd think that's a regression.

A ClientHello that changes server_name across a HelloRetryRequest is now rejected with illegal_parameter. Previously it was OpenSSL's problem.

+1

Malformed server_name extensions now read as absent (may select the wildcard identity) instead of relying on OpenSSL's parse.

Hmm.. not sure about this one. Smells bad.

Alert selection for SNI failure moves from the servername callback to the ClientHello callback.

+1

Endpoint bytesReceived / packetsReceived under-count when JS closes a session inside the session callback.

This is a regression. The stats should be accurate at this point.

Beyond that, I think this PR could definitely have been better split into multiple commits but ok.

@jasnell

Copy link
Copy Markdown
Member

Overall, +1 but there are a few regressions to investigate.

@pimterry

Copy link
Copy Markdown
MemberAuthor

Code updated. I've replied on the inline points above, but the other remaining notes from your larger comment:

When no ALPN is configured on the server, the failure mode moves from OnSelectAlpn returning NOACK to the ClientHello callback continuing with no Application.

This one I'm not convinced about. Currently we will fail the handshake if an ALPN that the server does not recognize is presented. With this, we proceed with the handshake and likely fail later. I'd think that's a regression.

I think there's a real issue here, but your description is a misreading of the summary. The problem being highlighted is only reachable when the server is explicitly configured with no ALPN protocols (alpn: []) and then a client connects. With this PR, actual ALPN mismatches did still fail the handshake, it was just that invalid ALPN config in the server made invalid application decisions.

I've now tighten this up, with a validation to server creation, to just reject that empty ALPN list at source instead so this is unreachable. I think ALPN is now correctly strict everywhere for QUIC.

Malformed server_name extensions now read as absent (may select the wildcard identity) instead of relying on OpenSSL's parse.

Hmm.. not sure about this one. Smells bad.

Yes, that was a bug. Server name handling now distinguishes the three states (valid SNI/no SNI/malformed SNI) and handles them correctly (do SNI/use wildcard SNI/fail handshake).

Endpoint bytesReceived / packetsReceived under-count when JS closes a session inside the session callback.

This is a regression. The stats should be accurate at this point.

Agreed, now fixed.

Comment threadlib/internal/quic/quic.js
@pimterry

Copy link
Copy Markdown
MemberAuthor

I've now added some proper benchmarks, one for pure connection setup (separately for QUIC vs H3) and one for single HTTP request time (separately for 1RTT & 0RTT). Results on my machine say no significant changes:

./node benchmark/compare.js --old ./node-old --new ./node-new --analyze -- quic
[00:18:50|% 100| 2/2 files | 60/60 runs | 4/4 configs]: Done
confidence improvement accuracy (*) (**) (***)
quic/h3-request.js n=500 mode='0rtt' +0.35 % ±1.23% ±1.64% ±2.13%
quic/h3-request.js n=500 mode='1rtt' +0.11 % ±1.04% ±1.39% ±1.80%
quic/handshake.js n=1000 concurrency=1 protocol='raw' -0.31 % ±1.35% ±1.80% ±2.36%
quic/handshake.js n=1000 concurrency=10 protocol='raw' +0.60 % ±0.98% ±1.31% ±1.70%
quic/handshake.js n=1000 concurrency=1 protocol='h3' * +0.97 % ±0.82% ±1.09% ±1.42%
quic/handshake.js n=1000 concurrency=10 protocol='h3' -0.47 % ±1.06% ±1.42% ±1.86%
-1.8% 0% +1.8%
quic/h3-request.js n=500 mode='0rtt' ░░░░░░░░░░|▓▓▓░░░░░░░░░░░░░░ +0.35% quic/h3-request.js n=500 mode='1rtt' ░░░░░░░░░░|░░░░░░░░░░░░ +0.11% quic/handshake.js n=1000 concurrency=1 protocol='raw' ░░░░░░░░░░░░░░░▓▓▓|░░░░░░░░░░░ -0.31% quic/handshake.js n=1000 concurrency=10 protocol='raw' ░░░░|▓▓▓▓▓▓░░░░░░░░░░░ +0.60% quic/handshake.js n=1000 concurrency=1 protocol='h3' |██████████░░░░░░░░░ +0.97% *
quic/handshake.js n=1000 concurrency=10 protocol='h3' ░░░░░░░░░░░░▓▓▓▓▓|░░░░░░ -0.47% Rates were scaled by 1000x into HdrHistogram (3 significant figures).
Use --scale to adjust precision if needed.

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@jasnell

Copy link
Copy Markdown
Member

oh that new --analyze is so nice ;-) ...

@pimterrypimterry added the commit-queue-squash PRs the Commit Queue should land as one squashed commit. label Sep 2, 2026
@pimterry

Copy link
Copy Markdown
MemberAuthor

@nodejs/quic @nodejs/crypto I think this is good to go now, just needs a final approve covering the docs & benchmark commits.

@panvapanva added author ready PRs with CI started, the required approvals, and no outstanding review comments. commit-queue PRs queued for automated landing through the Commit Queue. labels Sep 2, 2026
@nodejs-github-bot
nodejs-github-bot merged commit 09f5aae into nodejs:mainSep 2, 2026
77 checks passed
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Landed in 09f5aae

@nodejs-github-botnodejs-github-bot removed the commit-queue PRs queued for automated landing through the Commit Queue. label Sep 2, 2026
aduh95 pushed a commit that referenced this pull request Sep 3, 2026
With this change, the TLS pause mechanisms used in node:tls are now made
generic, and used in QUIC to pause the handshake entirely for the
session event, thereby stopping almost all events being emitted
(everything except qlog) so that all the previously existing deferral
machinery is unnecessary.
This is useful immediately to clean up & simplify things, but also
medium term because this makes the 0RTT flow significantly clearer,
which will help with the upcoming dynamic attach for QUIC (which
requires deferring ticket processing & 0RTT delivery until after
user code).
As part of this, this changes the internal flow of how ALPN & SNI are
resolved. Instead of calculating them synchronously within the OpenSSL
callbacks, we precalculate them at the pause, and then the OpenSSL
callbacks just return the precalculated values.
This is intended to only create visible changes to node:quic. Though it
touches node:tls, it preserves all existing behaviour (including
callback order and other details).
Signed-off-by: Tim Perry <pimterry@gmail.com>
PR-URL: #65522
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Filip Skokan <panva.ip@gmail.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

author readyPRs with CI started, the required approvals, and no outstanding review comments.c++Issues and PRs that require attention from people who are familiar with C++.commit-queue-squashPRs the Commit Queue should land as one squashed commit.lib / srcIssues and PRs involving general changes in the lib/ or src/ directories.needs-ciPRs that need a full CI run.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@pimterry@nodejs-github-bot@jasnell@panva
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

quic: reuse TLS pause machinery to drop event deferral & improve 0RTT - #65522

Merged
nodejs-github-bot merged 5 commits into
nodejs:mainfrom
pimterry:pause-for-quic-0rtt
Sep 2, 2026
Merged

quic: reuse TLS pause machinery to drop event deferral & improve 0RTT#65522
nodejs-github-bot merged 5 commits into
nodejs:mainfrom
pimterry:pause-for-quic-0rtt

Conversation

@pimterry

Copy link
Copy Markdown
Member

With this change, the TLS suspend mechanism I introduced in #64827 becomes generic and we reuse it in QUIC. This adds new client hello structures, which hide the OpenSSL vs BoringSSL differences, and expose an interface for each handshake flow to continue, suspend or fail the handshake. While suspended they can access the details and validate/prepare according to their needs.

There's one immediate advantage here: this allows QUIC to reuse this existing mechanism to largely replace its event deferral system. In QUIC, we need to defer some handshake events (mostly relating to resumption & 0RTT, see #64132) until the handshake is sufficiently complete so that at the session event we can do some setup with the data already available, but before the actual data processing within starts.

To do this currently we have a whole event deferral mechanism and multi-step session ticket extraction & validation flow. Directly suspending the handshake reusing the existing mechanism from node:tls lets us delete that completely, do the simple thing instead, and get the same result.

This then has another medium-term benefit: as part of #63995 we will be adding dynamic attach, where the application protocol is decided dynamically on session creation, which requires deferring session ticket processing even further (because we don't even know which protocol to validate for). This mechanism gives us the structure in which to easily do that, giving us 0RTT + dynamic attach.

As part of this, this changes the internal flow of how ALPN & SNI are resolved for QUIC, and extracts ALPN helpers to share more logic between TLS/QUIC/DTLS. Instead of calculating them synchronously within the OpenSSL callbacks, we now precalculate them at the suspend point, and then the OpenSSL callbacks later just use the precalculated values.

This is intended to only create visible changes to node:quic. Though it touches node:tls & node:dtls, it's just commonizing logic and preserves all existing behaviour (including callback order and other details). In future I'd like to make some small breaking changes to both to bring these further inline, but that's tricky and we can ignore it for now.

With this change, the TLS pause mechanisms used in node:tls are now made
generic, and used in QUIC to pause the handshake entirely for the
session event, thereby stopping almost all events being emitted
(everything except qlog) so that all the previously existing deferral
machinery is unnecessary.
This is useful immediately to clean up & simplify things, but also
medium term because this makes the 0RTT flow significantly clearer,
which will help with the upcoming dynamic attach for QUIC (which
requires deferring ticket processing & 0RTT delivery until after
user code).
As part of this, this changes the internal flow of how ALPN & SNI are
resolved. Instead of calculating them synchronously within the OpenSSL
callbacks, we precalculate them at the pause, and then the OpenSSL
callbacks just return the precalculated values.
This is intended to only create visible changes to node:quic. Though it
touches node:tls, it preserves all existing behaviour (including
callback order and other details).
Signed-off-by: Tim Perry <pimterry@gmail.com>
@pimterry
pimterry requested a review from jasnellAugust 24, 2026 18:54
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Review requested:

  • @nodejs/crypto
  • @nodejs/gyp
  • @nodejs/quic

@nodejs-github-botnodejs-github-bot added c++ Issues and PRs that require attention from people who are familiar with C++. lib / src Issues and PRs involving general changes in the lib/ or src/ directories. needs-ci PRs that need a full CI run. labels Aug 24, 2026
@codecov

codecovBot commented Aug 24, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 60.00000% with 46 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.11%. Comparing base (f509cf1) to head (e641814).
⚠️ Report is 140 commits behind head on main.

Files with missing linesPatch %Lines
src/crypto/crypto_client_hello.cc44.57%43 Missing and 3 partials ⚠️
Additional details and impacted files
@@ Coverage Diff @@## main #65522 +/- ##
==========================================
- Coverage 90.14% 90.11% -0.03% 
==========================================
Files 751 753 +2 Lines 253585 253733 +148 Branches 47772 47818 +46 ==========================================
+ Hits 228596 228661 +65 - Misses 16228 16308 +80 - Partials 8761 8764 +3 
Files with missing linesCoverage Δ
lib/internal/quic/quic.js100.00% <100.00%> (ø)
src/crypto/crypto_client_hello.h100.00% <100.00%> (ø)
src/crypto/crypto_context.cc71.59% <ø> (-0.07%)⬇️
src/crypto/crypto_context.h100.00% <ø> (ø)
src/crypto/crypto_tls.cc78.78% <100.00%> (+0.01%)⬆️
src/crypto/crypto_client_hello.cc44.57% <44.57%> (ø)

... and 40 files with indirect coverage changes

🚀 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.

@jasnell

Copy link
Copy Markdown
Member

Rather than focusing the PR description on the benefits, can you explain what the behavioral / structural differences here are?

Comment threadsrc/crypto/crypto_client_hello.h Outdated
Comment threadsrc/crypto/crypto_client_hello.h Outdated
Comment threadsrc/crypto/crypto_client_hello.h Outdated
Comment threadsrc/crypto/crypto_client_hello.h Outdated
Comment threadsrc/crypto/crypto_tls.cc Outdated
Comment threadsrc/quic/application.cc Outdated
Comment threadsrc/quic/endpoint.cc
Comment threadsrc/quic/session.cc Outdated
@jasnell

Copy link
Copy Markdown
Member

Ok, I had an agent summarize out the actual changes here. Let me step through them. The quoted bits here are from the agents summary:

The session event is now delivered before the TLS handshake proceeds past the ClientHello, rather than after the first packet is fully processed. No stream, datagram, keylog, origin, application, goaway or datagram-status event can precede it — this is now a protocol-level guarantee rather than a replay artifact.

I know this is the key goal here with your eventual dynamic attach goal.. essentially pause here and let the JS side set things up. My goal has been to defer calling out to JS until we absolutely need to, handling as much of the handshake as possible in C++ as we can.

I'm still not entirely sold on dynamic attach yet. We know at this point that the ALPN is either h3 or something else, and we know that h3 maps to Http3Application and something else maps to the default behavior. Why call out to JavaScript to have it select and install the application? That seems entirely unnecessary and not a performance win. But I guess that's an argument for the larger PR HTTP3/QUIC split PR.

Per-SNI-identity alpn lists are now honoured. Previously the default endpoint context's alpn was always used. (behaviour change

+1 ... this has been on my list to revisit for a while.

Keylog lines are emitted in real time rather than replayed, so their interleaving with other events changes.

Seems fine. We'll need to revisit keylog/qlog when all this is done anyway to ensure we're capturing everything correctly.

session.remoteTransportParams is populated at the session event (previously undefined).

+1

Session tickets with absent or malformed application data are now rejected rather than accepted optimistically

This is good. Absent/malformed application data should mean that the ticket is ignored and a renewed ticket is returned. If we just ignore, the client may continue to just keep trying to use the bad ticket.

When no ALPN is configured on the server, the failure mode moves from OnSelectAlpn returning NOACK to the ClientHello callback continuing with no Application.

This one I'm not convinced about. Currently we will fail the handshake if an ALPN that the server does not recognize is presented. With this, we proceed with the handshake and likely fail later. I'd think that's a regression.

A ClientHello that changes server_name across a HelloRetryRequest is now rejected with illegal_parameter. Previously it was OpenSSL's problem.

+1

Malformed server_name extensions now read as absent (may select the wildcard identity) instead of relying on OpenSSL's parse.

Hmm.. not sure about this one. Smells bad.

Alert selection for SNI failure moves from the servername callback to the ClientHello callback.

+1

Endpoint bytesReceived / packetsReceived under-count when JS closes a session inside the session callback.

This is a regression. The stats should be accurate at this point.

Beyond that, I think this PR could definitely have been better split into multiple commits but ok.

@jasnell

Copy link
Copy Markdown
Member

Overall, +1 but there are a few regressions to investigate.

@pimterry

Copy link
Copy Markdown
MemberAuthor

Code updated. I've replied on the inline points above, but the other remaining notes from your larger comment:

When no ALPN is configured on the server, the failure mode moves from OnSelectAlpn returning NOACK to the ClientHello callback continuing with no Application.

This one I'm not convinced about. Currently we will fail the handshake if an ALPN that the server does not recognize is presented. With this, we proceed with the handshake and likely fail later. I'd think that's a regression.

I think there's a real issue here, but your description is a misreading of the summary. The problem being highlighted is only reachable when the server is explicitly configured with no ALPN protocols (alpn: []) and then a client connects. With this PR, actual ALPN mismatches did still fail the handshake, it was just that invalid ALPN config in the server made invalid application decisions.

I've now tighten this up, with a validation to server creation, to just reject that empty ALPN list at source instead so this is unreachable. I think ALPN is now correctly strict everywhere for QUIC.

Malformed server_name extensions now read as absent (may select the wildcard identity) instead of relying on OpenSSL's parse.

Hmm.. not sure about this one. Smells bad.

Yes, that was a bug. Server name handling now distinguishes the three states (valid SNI/no SNI/malformed SNI) and handles them correctly (do SNI/use wildcard SNI/fail handshake).

Endpoint bytesReceived / packetsReceived under-count when JS closes a session inside the session callback.

This is a regression. The stats should be accurate at this point.

Agreed, now fixed.

Comment threadlib/internal/quic/quic.js
@pimterry

Copy link
Copy Markdown
MemberAuthor

I've now added some proper benchmarks, one for pure connection setup (separately for QUIC vs H3) and one for single HTTP request time (separately for 1RTT & 0RTT). Results on my machine say no significant changes:

./node benchmark/compare.js --old ./node-old --new ./node-new --analyze -- quic
[00:18:50|% 100| 2/2 files | 60/60 runs | 4/4 configs]: Done
confidence improvement accuracy (*) (**) (***)
quic/h3-request.js n=500 mode='0rtt' +0.35 % ±1.23% ±1.64% ±2.13%
quic/h3-request.js n=500 mode='1rtt' +0.11 % ±1.04% ±1.39% ±1.80%
quic/handshake.js n=1000 concurrency=1 protocol='raw' -0.31 % ±1.35% ±1.80% ±2.36%
quic/handshake.js n=1000 concurrency=10 protocol='raw' +0.60 % ±0.98% ±1.31% ±1.70%
quic/handshake.js n=1000 concurrency=1 protocol='h3' * +0.97 % ±0.82% ±1.09% ±1.42%
quic/handshake.js n=1000 concurrency=10 protocol='h3' -0.47 % ±1.06% ±1.42% ±1.86%
-1.8% 0% +1.8%
quic/h3-request.js n=500 mode='0rtt' ░░░░░░░░░░|▓▓▓░░░░░░░░░░░░░░ +0.35% quic/h3-request.js n=500 mode='1rtt' ░░░░░░░░░░|░░░░░░░░░░░░ +0.11% quic/handshake.js n=1000 concurrency=1 protocol='raw' ░░░░░░░░░░░░░░░▓▓▓|░░░░░░░░░░░ -0.31% quic/handshake.js n=1000 concurrency=10 protocol='raw' ░░░░|▓▓▓▓▓▓░░░░░░░░░░░ +0.60% quic/handshake.js n=1000 concurrency=1 protocol='h3' |██████████░░░░░░░░░ +0.97% *
quic/handshake.js n=1000 concurrency=10 protocol='h3' ░░░░░░░░░░░░▓▓▓▓▓|░░░░░░ -0.47% Rates were scaled by 1000x into HdrHistogram (3 significant figures).
Use --scale to adjust precision if needed.

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@jasnell

Copy link
Copy Markdown
Member

oh that new --analyze is so nice ;-) ...

@pimterrypimterry added the commit-queue-squash PRs the Commit Queue should land as one squashed commit. label Sep 2, 2026
@pimterry

Copy link
Copy Markdown
MemberAuthor

@nodejs/quic @nodejs/crypto I think this is good to go now, just needs a final approve covering the docs & benchmark commits.

@panvapanva added author ready PRs with CI started, the required approvals, and no outstanding review comments. commit-queue PRs queued for automated landing through the Commit Queue. labels Sep 2, 2026
@nodejs-github-bot
nodejs-github-bot merged commit 09f5aae into nodejs:mainSep 2, 2026
77 checks passed
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Landed in 09f5aae

@nodejs-github-botnodejs-github-bot removed the commit-queue PRs queued for automated landing through the Commit Queue. label Sep 2, 2026
aduh95 pushed a commit that referenced this pull request Sep 3, 2026
With this change, the TLS pause mechanisms used in node:tls are now made
generic, and used in QUIC to pause the handshake entirely for the
session event, thereby stopping almost all events being emitted
(everything except qlog) so that all the previously existing deferral
machinery is unnecessary.
This is useful immediately to clean up & simplify things, but also
medium term because this makes the 0RTT flow significantly clearer,
which will help with the upcoming dynamic attach for QUIC (which
requires deferring ticket processing & 0RTT delivery until after
user code).
As part of this, this changes the internal flow of how ALPN & SNI are
resolved. Instead of calculating them synchronously within the OpenSSL
callbacks, we precalculate them at the pause, and then the OpenSSL
callbacks just return the precalculated values.
This is intended to only create visible changes to node:quic. Though it
touches node:tls, it preserves all existing behaviour (including
callback order and other details).
Signed-off-by: Tim Perry <pimterry@gmail.com>
PR-URL: #65522
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Filip Skokan <panva.ip@gmail.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

author readyPRs with CI started, the required approvals, and no outstanding review comments.c++Issues and PRs that require attention from people who are familiar with C++.commit-queue-squashPRs the Commit Queue should land as one squashed commit.lib / srcIssues and PRs involving general changes in the lib/ or src/ directories.needs-ciPRs that need a full CI run.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@pimterry@nodejs-github-bot@jasnell@panva
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

quic: reuse TLS pause machinery to drop event deferral & improve 0RTT - #65522

Merged
nodejs-github-bot merged 5 commits into
nodejs:mainfrom
pimterry:pause-for-quic-0rtt
Sep 2, 2026
Merged

quic: reuse TLS pause machinery to drop event deferral & improve 0RTT#65522
nodejs-github-bot merged 5 commits into
nodejs:mainfrom
pimterry:pause-for-quic-0rtt

Conversation

@pimterry

Copy link
Copy Markdown
Member

With this change, the TLS suspend mechanism I introduced in #64827 becomes generic and we reuse it in QUIC. This adds new client hello structures, which hide the OpenSSL vs BoringSSL differences, and expose an interface for each handshake flow to continue, suspend or fail the handshake. While suspended they can access the details and validate/prepare according to their needs.

There's one immediate advantage here: this allows QUIC to reuse this existing mechanism to largely replace its event deferral system. In QUIC, we need to defer some handshake events (mostly relating to resumption & 0RTT, see #64132) until the handshake is sufficiently complete so that at the session event we can do some setup with the data already available, but before the actual data processing within starts.

To do this currently we have a whole event deferral mechanism and multi-step session ticket extraction & validation flow. Directly suspending the handshake reusing the existing mechanism from node:tls lets us delete that completely, do the simple thing instead, and get the same result.

This then has another medium-term benefit: as part of #63995 we will be adding dynamic attach, where the application protocol is decided dynamically on session creation, which requires deferring session ticket processing even further (because we don't even know which protocol to validate for). This mechanism gives us the structure in which to easily do that, giving us 0RTT + dynamic attach.

As part of this, this changes the internal flow of how ALPN & SNI are resolved for QUIC, and extracts ALPN helpers to share more logic between TLS/QUIC/DTLS. Instead of calculating them synchronously within the OpenSSL callbacks, we now precalculate them at the suspend point, and then the OpenSSL callbacks later just use the precalculated values.

This is intended to only create visible changes to node:quic. Though it touches node:tls & node:dtls, it's just commonizing logic and preserves all existing behaviour (including callback order and other details). In future I'd like to make some small breaking changes to both to bring these further inline, but that's tricky and we can ignore it for now.

With this change, the TLS pause mechanisms used in node:tls are now made
generic, and used in QUIC to pause the handshake entirely for the
session event, thereby stopping almost all events being emitted
(everything except qlog) so that all the previously existing deferral
machinery is unnecessary.
This is useful immediately to clean up & simplify things, but also
medium term because this makes the 0RTT flow significantly clearer,
which will help with the upcoming dynamic attach for QUIC (which
requires deferring ticket processing & 0RTT delivery until after
user code).
As part of this, this changes the internal flow of how ALPN & SNI are
resolved. Instead of calculating them synchronously within the OpenSSL
callbacks, we precalculate them at the pause, and then the OpenSSL
callbacks just return the precalculated values.
This is intended to only create visible changes to node:quic. Though it
touches node:tls, it preserves all existing behaviour (including
callback order and other details).
Signed-off-by: Tim Perry <pimterry@gmail.com>
@pimterry
pimterry requested a review from jasnellAugust 24, 2026 18:54
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Review requested:

  • @nodejs/crypto
  • @nodejs/gyp
  • @nodejs/quic

@nodejs-github-botnodejs-github-bot added c++ Issues and PRs that require attention from people who are familiar with C++. lib / src Issues and PRs involving general changes in the lib/ or src/ directories. needs-ci PRs that need a full CI run. labels Aug 24, 2026
@codecov

codecovBot commented Aug 24, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 60.00000% with 46 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.11%. Comparing base (f509cf1) to head (e641814).
⚠️ Report is 140 commits behind head on main.

Files with missing linesPatch %Lines
src/crypto/crypto_client_hello.cc44.57%43 Missing and 3 partials ⚠️
Additional details and impacted files
@@ Coverage Diff @@## main #65522 +/- ##
==========================================
- Coverage 90.14% 90.11% -0.03% 
==========================================
Files 751 753 +2 Lines 253585 253733 +148 Branches 47772 47818 +46 ==========================================
+ Hits 228596 228661 +65 - Misses 16228 16308 +80 - Partials 8761 8764 +3 
Files with missing linesCoverage Δ
lib/internal/quic/quic.js100.00% <100.00%> (ø)
src/crypto/crypto_client_hello.h100.00% <100.00%> (ø)
src/crypto/crypto_context.cc71.59% <ø> (-0.07%)⬇️
src/crypto/crypto_context.h100.00% <ø> (ø)
src/crypto/crypto_tls.cc78.78% <100.00%> (+0.01%)⬆️
src/crypto/crypto_client_hello.cc44.57% <44.57%> (ø)

... and 40 files with indirect coverage changes

🚀 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.

@jasnell

Copy link
Copy Markdown
Member

Rather than focusing the PR description on the benefits, can you explain what the behavioral / structural differences here are?

Comment threadsrc/crypto/crypto_client_hello.h Outdated
Comment threadsrc/crypto/crypto_client_hello.h Outdated
Comment threadsrc/crypto/crypto_client_hello.h Outdated
Comment threadsrc/crypto/crypto_client_hello.h Outdated
Comment threadsrc/crypto/crypto_tls.cc Outdated
Comment threadsrc/quic/application.cc Outdated
Comment threadsrc/quic/endpoint.cc
Comment threadsrc/quic/session.cc Outdated
@jasnell

Copy link
Copy Markdown
Member

Ok, I had an agent summarize out the actual changes here. Let me step through them. The quoted bits here are from the agents summary:

The session event is now delivered before the TLS handshake proceeds past the ClientHello, rather than after the first packet is fully processed. No stream, datagram, keylog, origin, application, goaway or datagram-status event can precede it — this is now a protocol-level guarantee rather than a replay artifact.

I know this is the key goal here with your eventual dynamic attach goal.. essentially pause here and let the JS side set things up. My goal has been to defer calling out to JS until we absolutely need to, handling as much of the handshake as possible in C++ as we can.

I'm still not entirely sold on dynamic attach yet. We know at this point that the ALPN is either h3 or something else, and we know that h3 maps to Http3Application and something else maps to the default behavior. Why call out to JavaScript to have it select and install the application? That seems entirely unnecessary and not a performance win. But I guess that's an argument for the larger PR HTTP3/QUIC split PR.

Per-SNI-identity alpn lists are now honoured. Previously the default endpoint context's alpn was always used. (behaviour change

+1 ... this has been on my list to revisit for a while.

Keylog lines are emitted in real time rather than replayed, so their interleaving with other events changes.

Seems fine. We'll need to revisit keylog/qlog when all this is done anyway to ensure we're capturing everything correctly.

session.remoteTransportParams is populated at the session event (previously undefined).

+1

Session tickets with absent or malformed application data are now rejected rather than accepted optimistically

This is good. Absent/malformed application data should mean that the ticket is ignored and a renewed ticket is returned. If we just ignore, the client may continue to just keep trying to use the bad ticket.

When no ALPN is configured on the server, the failure mode moves from OnSelectAlpn returning NOACK to the ClientHello callback continuing with no Application.

This one I'm not convinced about. Currently we will fail the handshake if an ALPN that the server does not recognize is presented. With this, we proceed with the handshake and likely fail later. I'd think that's a regression.

A ClientHello that changes server_name across a HelloRetryRequest is now rejected with illegal_parameter. Previously it was OpenSSL's problem.

+1

Malformed server_name extensions now read as absent (may select the wildcard identity) instead of relying on OpenSSL's parse.

Hmm.. not sure about this one. Smells bad.

Alert selection for SNI failure moves from the servername callback to the ClientHello callback.

+1

Endpoint bytesReceived / packetsReceived under-count when JS closes a session inside the session callback.

This is a regression. The stats should be accurate at this point.

Beyond that, I think this PR could definitely have been better split into multiple commits but ok.

@jasnell

Copy link
Copy Markdown
Member

Overall, +1 but there are a few regressions to investigate.

@pimterry

Copy link
Copy Markdown
MemberAuthor

Code updated. I've replied on the inline points above, but the other remaining notes from your larger comment:

When no ALPN is configured on the server, the failure mode moves from OnSelectAlpn returning NOACK to the ClientHello callback continuing with no Application.

This one I'm not convinced about. Currently we will fail the handshake if an ALPN that the server does not recognize is presented. With this, we proceed with the handshake and likely fail later. I'd think that's a regression.

I think there's a real issue here, but your description is a misreading of the summary. The problem being highlighted is only reachable when the server is explicitly configured with no ALPN protocols (alpn: []) and then a client connects. With this PR, actual ALPN mismatches did still fail the handshake, it was just that invalid ALPN config in the server made invalid application decisions.

I've now tighten this up, with a validation to server creation, to just reject that empty ALPN list at source instead so this is unreachable. I think ALPN is now correctly strict everywhere for QUIC.

Malformed server_name extensions now read as absent (may select the wildcard identity) instead of relying on OpenSSL's parse.

Hmm.. not sure about this one. Smells bad.

Yes, that was a bug. Server name handling now distinguishes the three states (valid SNI/no SNI/malformed SNI) and handles them correctly (do SNI/use wildcard SNI/fail handshake).

Endpoint bytesReceived / packetsReceived under-count when JS closes a session inside the session callback.

This is a regression. The stats should be accurate at this point.

Agreed, now fixed.

Comment threadlib/internal/quic/quic.js
@pimterry

Copy link
Copy Markdown
MemberAuthor

I've now added some proper benchmarks, one for pure connection setup (separately for QUIC vs H3) and one for single HTTP request time (separately for 1RTT & 0RTT). Results on my machine say no significant changes:

./node benchmark/compare.js --old ./node-old --new ./node-new --analyze -- quic
[00:18:50|% 100| 2/2 files | 60/60 runs | 4/4 configs]: Done
confidence improvement accuracy (*) (**) (***)
quic/h3-request.js n=500 mode='0rtt' +0.35 % ±1.23% ±1.64% ±2.13%
quic/h3-request.js n=500 mode='1rtt' +0.11 % ±1.04% ±1.39% ±1.80%
quic/handshake.js n=1000 concurrency=1 protocol='raw' -0.31 % ±1.35% ±1.80% ±2.36%
quic/handshake.js n=1000 concurrency=10 protocol='raw' +0.60 % ±0.98% ±1.31% ±1.70%
quic/handshake.js n=1000 concurrency=1 protocol='h3' * +0.97 % ±0.82% ±1.09% ±1.42%
quic/handshake.js n=1000 concurrency=10 protocol='h3' -0.47 % ±1.06% ±1.42% ±1.86%
-1.8% 0% +1.8%
quic/h3-request.js n=500 mode='0rtt' ░░░░░░░░░░|▓▓▓░░░░░░░░░░░░░░ +0.35% quic/h3-request.js n=500 mode='1rtt' ░░░░░░░░░░|░░░░░░░░░░░░ +0.11% quic/handshake.js n=1000 concurrency=1 protocol='raw' ░░░░░░░░░░░░░░░▓▓▓|░░░░░░░░░░░ -0.31% quic/handshake.js n=1000 concurrency=10 protocol='raw' ░░░░|▓▓▓▓▓▓░░░░░░░░░░░ +0.60% quic/handshake.js n=1000 concurrency=1 protocol='h3' |██████████░░░░░░░░░ +0.97% *
quic/handshake.js n=1000 concurrency=10 protocol='h3' ░░░░░░░░░░░░▓▓▓▓▓|░░░░░░ -0.47% Rates were scaled by 1000x into HdrHistogram (3 significant figures).
Use --scale to adjust precision if needed.

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@jasnell

Copy link
Copy Markdown
Member

oh that new --analyze is so nice ;-) ...

@pimterrypimterry added the commit-queue-squash PRs the Commit Queue should land as one squashed commit. label Sep 2, 2026
@pimterry

Copy link
Copy Markdown
MemberAuthor

@nodejs/quic @nodejs/crypto I think this is good to go now, just needs a final approve covering the docs & benchmark commits.

@panvapanva added author ready PRs with CI started, the required approvals, and no outstanding review comments. commit-queue PRs queued for automated landing through the Commit Queue. labels Sep 2, 2026
@nodejs-github-bot
nodejs-github-bot merged commit 09f5aae into nodejs:mainSep 2, 2026
77 checks passed
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Landed in 09f5aae

@nodejs-github-botnodejs-github-bot removed the commit-queue PRs queued for automated landing through the Commit Queue. label Sep 2, 2026
aduh95 pushed a commit that referenced this pull request Sep 3, 2026
With this change, the TLS pause mechanisms used in node:tls are now made
generic, and used in QUIC to pause the handshake entirely for the
session event, thereby stopping almost all events being emitted
(everything except qlog) so that all the previously existing deferral
machinery is unnecessary.
This is useful immediately to clean up & simplify things, but also
medium term because this makes the 0RTT flow significantly clearer,
which will help with the upcoming dynamic attach for QUIC (which
requires deferring ticket processing & 0RTT delivery until after
user code).
As part of this, this changes the internal flow of how ALPN & SNI are
resolved. Instead of calculating them synchronously within the OpenSSL
callbacks, we precalculate them at the pause, and then the OpenSSL
callbacks just return the precalculated values.
This is intended to only create visible changes to node:quic. Though it
touches node:tls, it preserves all existing behaviour (including
callback order and other details).
Signed-off-by: Tim Perry <pimterry@gmail.com>
PR-URL: #65522
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Filip Skokan <panva.ip@gmail.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

author readyPRs with CI started, the required approvals, and no outstanding review comments.c++Issues and PRs that require attention from people who are familiar with C++.commit-queue-squashPRs the Commit Queue should land as one squashed commit.lib / srcIssues and PRs involving general changes in the lib/ or src/ directories.needs-ciPRs that need a full CI run.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@pimterry@nodejs-github-bot@jasnell@panva