@ 261105.16raw
current

QUIC

Stability: 1.0 - Early development

The ’node:quic’ module provides an implementation of the QUIC protocol. To access it, start Node.js with the --experimental-quic option and:

import quic from 'node:quic';
const quic = require('node:quic');

The module is only available under the node: scheme.

Overview

The quic module provides APIs for creating QUIC clients and servers.

Relevant RFCs and specifications

The QUIC and HTTP/3 protocols are defined by a collection of RFCs produced primarily by the IETF QUIC Working Group. A familiarity with these documents is strongly recommended for users of this module.

Core QUIC transport:

Core HTTP/3:

QUIC extensions:

HTTP/3 extensions:

Operational and informational:

Architecture

The quic module is built around three core abstractions:

Unlike traditional TCP-based protocols, QUIC “connections” are not inherently tied to a specific local port / remote port pair. A session is initiated via a QUIC endpoint but may be migrated to a different local or remote address over its lifetime, outlive the endpoint that created it, and may even be associated with multiple endpoints simultaneously. This flexibility allows for advanced use cases such as connection migration, multi-homing, and load balancing. Most often, however, a simple one-to-one relationship between endpoint and session is sufficient.

Integrated TLS 1.3

The QUIC protocol integrates TLS 1.3 directly into the protocol for connection establishment and security. The quic module’s API reflects this integration by exposing TLS-related information and configuration options. It is currently not possible to use QUIC without TLS or to use a different version of TLS.

Every QUIC session starts with the client and server performing a TLS handshake to negotiate the application protocol (via ALPN), authenticate the server (and optionally the client), exchange transport parameters, and establish shared keys for encryption.

Certificate size and handshake performance

QUIC includes an anti-amplification limit (RFC 9000 Section 8.1) that restricts the server to sending at most three times the data received from the client before the client’s address is validated. Because the client’s Initial packet is typically around 1200 bytes, the server can send at most approximately 3600 bytes before it must wait for the client to acknowledge.

The server’s initial response is dominated by its TLS certificate chain. If the certificate chain exceeds the amplification limit, the handshake requires an additional round trip — the server must pause, wait for the client’s acknowledgement, and then continue sending the remainder of the certificate. This eliminates QUIC’s 1-RTT handshake advantage over TCP+TLS and can add 50–100 ms or more of latency on the first connection, depending on the network path.

To avoid this, servers should use compact certificate chains:

Certificate compression (RFC 8879) can also address this issue by compressing the certificate chain during the handshake, often keeping the server’s Certificate message within the amplification limit and avoiding the extra round trip. Certificate compression is opt-in via the certificateCompression TLS option and is disabled by default. When enabled, it applies to both the server’s certificate and, for mutual TLS, the client’s certificate.

Rate limiting

QUIC endpoints include built-in rate limiting to protect against denial-of-service attacks. There are two layers of defense:

Global rate limits cap the total rate of stateless responses that the endpoint will send, regardless of the source address. These protect against floods from spoofed source IP addresses, where an attacker rotates through many fake source addresses to bypass per-host limits. Four types of stateless responses are independently rate-limited:

Each rate limit uses a token bucket: the endpoint can send up to the burst capacity instantly, and tokens refill at the configured rate per second. When the bucket is empty, additional responses of that type are silently dropped. The defaults (100 per second, burst of 200) are suitable for most deployments.

Per-host session creation rate limits cap how fast a single remote address can create new sessions. This is tracked per validated remote address and prevents a single client from churning through sessions (rapidly connecting and disconnecting) to consume server resources. Configurable via endpointOptions.sessionCreationRate and endpointOptions.sessionCreationBurst. The defaults (50 per second, burst of 100) are generous enough for legitimate traffic patterns. For benchmarking scenarios where traffic comes from a single source, increase these values.

In addition to rate limiting, the endpoint supports concurrent connection limits via maxConnectionsPerHost and maxConnectionsTotal, and a busy mode via endpoint.busy that rejects all new connections.

Rate limiting activity can be monitored through the endpoint’s statistics object. Each rate limiter has a corresponding counter (e.g., endpoint.stats.retryRateLimited, endpoint.stats.sessionCreationRateLimited) that tracks how many responses were dropped. A non-zero value indicates the rate limiter is actively protecting the endpoint.

Block lists

Endpoints can filter incoming packets by source address using a net.BlockList. The block list is checked before any QUIC processing occurs, so blocked packets consume no resources beyond the check itself.

In deny mode (the default), packets from addresses in the list are dropped:

import { BlockList } from 'node:net';
import { listen } from 'node:quic';

const blocked = new BlockList();
blocked.addSubnet('192.168.1.0', 24);  // Block an entire subnet
blocked.addAddress('10.0.0.5');        // Block a specific address

const endpoint = await listen(onSession, {
  endpoint: {
    blockList: blocked,
    blockListPolicy: 'deny',
  },
  // ...
});

In allow mode, only packets from addresses in the list are accepted:

const trusted = new BlockList();
trusted.addSubnet('10.0.0.0', 8);

const endpoint = await listen(onSession, {
  endpoint: {
    blockList: trusted,
    blockListPolicy: 'allow',
  },
  // ...
});

The block list is evaluated live — rules added or removed after the endpoint is created take effect immediately. The endpoint.stats.packetsBlocked counter tracks how many packets have been dropped by the filter.

Applications

Every QuicSession is associated with a single application protocol, negotiated via ALPN during the TLS handshake. The quic module is designed to be application-agnostic in general but includes built-in support for HTTP/3 as a specific application protocol. When using HTTP/3, the quic module provides additional APIs for handling HTTP/3-specific features such as headers, trailers, and prioritization. For other application protocols, users can implement their own message framing and multiplexing on top of the core QUIC transport features.

When initiating a TLS handshake, the client will include a list of supported ALPN protocols in the ClientHello. The server selects one of these protocols (if any) and includes it in the ServerHello. The negotiated protocol determines how the QuicSession and QuicStream APIs behave. For example, when the h3 protocol is negotiated for HTTP/3, the QuicSession and QuicStream will support HTTP/3-specific features.

Currently, the quic module only supports HTTP/3 as a built-in application protocol. All other protocols must be implemented by the user on top of the provided JavaScript API.

Configuration

The QUIC API is designed to be flexible and highly configurable to support a wide range of use cases. Users can configure various aspects of the QUIC transport, TLS handshake, and application behavior via options passed to the quic.connect() and quic.listen() functions, as well as dynamically on QuicEndpoint and QuicSession instances. The API also provides access to detailed statistics and events for monitoring and debugging.

QUIC transport parameters are exchanged during the TLS handshake to negotiate various transport-level settings such as maximum stream counts, idle timeouts, and datagram support. The quic module allows users to configure the transport parameters their endpoint advertises to peers, as well as access the transport parameters advertised by peers. These configure the capabilities and limits of the QUIC connection in coordination with the peer.

A rich set of local settings is also available for configuring the behavior of the local endpoint and sessions. These include settings for connection limits, congestion control, stream prioritization, and more.

Callbacks and Promises

The quic module uses a combination of callbacks and promises for asynchronous operations. For example, initiating a connection with quic.connect() returns a promise for the established session, while incoming sessions on the server side are handled via a callback passed to quic.listen(). Within a session, events such as incoming streams, datagrams, and session state changes are handled via callbacks on the QuicSession instance. Promises are used for operations that have a clear completion point, such as completion of the TLS handshake or graceful closure of a session.

All callbacks are invoked synchronously and may either return synchronously or return a promise. If a callback returns a promise that rejects, or throws an error, the object will be destroyed with the error as the reason if an onerror callback is not specified.

Streams

Streams are the primary data-carrying abstraction in QUIC. A stream can be initiated by either the local endpoint or the remote peer once a session is established.

Streams can be either bidirectional (data flows in both directions) or unidirectional (data flows in only one direction). The quic module provides separate APIs for creating each kind: session.createBidirectionalStream() and session.createUnidirectionalStream(). Streams initiated by a remote peer are delivered via the session.onstream callback. When the negotiated application protocol supports the stream-level callbacks (e.g. HTTP/3) and an onheaders callback is configured, incoming streams can instead be consumed entirely through it and registering onstream is optional.

There are two ways to write data to a stream:

These two approaches are mutually exclusive for a given stream.

Reading is done by iterating the stream as an async iterable. Each iteration yields a batch of Uint8Array chunks:

for await (const chunks of stream) {
  for (const chunk of chunks) {
    // Process each Uint8Array chunk
  }
}

Only one async iterator can be obtained per stream. The stream is also compatible with node:stream/iter utilities such as Stream.bytes(), Stream.text(), and Stream.pipeTo().

Consuming a stream is what returns flow-control credit to the peer, so a stream whose payload is not wanted should still be read to completion. Use Stream.dump() to read the stream without retaining any of it.

Datagrams

In addition to streams, QUIC supports unreliable datagrams (RFC 9221) for use cases that require low-latency, best-effort messaging.

Datagram support is enabled at two levels. At the QUIC transport level, both peers must advertise a non-zero maxDatagramFrameSize transport parameter during the handshake. For HTTP/3 sessions, both peers must additionally set application.enableDatagrams to true, which exchanges the SETTINGS_H3_DATAGRAM setting on the HTTP/3 control stream.

A datagram is sent with a single call to session.sendDatagram(). Each datagram must fit within a single QUIC packet — datagrams cannot be fragmented. The maximum payload size is determined by the peer’s maxDatagramFrameSize and the path MTU. If a datagram is too large or the peer does not support datagrams, sendDatagram() returns 0n rather than throwing an error.

There is no guarantee of delivery. Datagrams may be lost, duplicated, or delivered out of order. The session.ondatagramstatus callback reports whether each sent datagram was 'acknowledged', 'lost', or 'abandoned' (never sent on the wire).

0-RTT early data and session resumption

QUIC supports 0-RTT early data, allowing a client that has previously connected to a server to send application data with its very first packet, without waiting for the handshake to complete. This can eliminate a full round-trip of latency on reconnection.

Two pieces of state from a prior connection make this possible:

If the server accepts the session ticket, any data sent before the handshake completes is 0-RTT early data. On the server side, stream.early is true for streams carrying early data. The server can reject the 0-RTT attempt (for example, if its configuration has changed since the ticket was issued). When this happens, all streams opened during the 0-RTT phase are destroyed and the client’s session.onearlyrejected callback fires. The connection falls back to a normal 1-RTT handshake and the application can reopen streams.

Early data is less secure than data sent after the handshake completes — it can potentially be replayed by an attacker. Applications should treat 0-RTT data with appropriate caution and avoid performing non-idempotent operations during the early data phase.

Connection lifecycle

A typical client session progresses through these stages:

  1. Call quic.connect() with a server address and options. This returns a QuicSession.
  2. The TLS handshake runs automatically. session.opened resolves when the handshake completes, providing the negotiated ALPN, cipher, and certificate validation results.
  3. Open streams, send datagrams, and exchange data.
  4. Call session.close() to initiate a graceful shutdown. Existing streams are allowed to finish, then the session is destroyed. The returned promise (also available as session.closed) resolves when teardown is complete.

On the server side, call quic.listen() with a callback. The callback fires for each incoming session after the TLS handshake begins. Incoming streams arrive via the session.onstream callback, or, for HTTP/3 sessions with an onheaders callback configured, directly through that callback (see the minimal HTTP/3 server example).

session.destroy() is available for immediate teardown — all open streams are destroyed and the session is closed without waiting for them to finish.

QuicEndpoint and QuicSession support Symbol.asyncDispose, so they can be used with await using for automatic cleanup.

Error handling

Errors in the quic module are communicated through two complementary mechanisms: the onerror callback and the closed promise.

Both QuicSession and QuicStream expose an optional onerror callback. When a session or stream is destroyed with an error — including errors thrown by other user callbacks — the onerror callback is invoked with the error before the object is torn down. Setting onerror also marks the closed promise as handled, preventing unhandled rejection warnings. If onerror is not set, the error is delivered solely through the rejection of the closed promise.

The QuicError class carries an explicit numeric QUIC error code (error.errorCode) alongside the usual message and code properties. When a QuicError is passed to stream.destroy() or writer.fail(), its errorCode is used in the RESET_STREAM or STOP_SENDING frame sent to the peer. Any other error type falls back to the negotiated protocol’s generic internal error code.

Permission model

When using the Permission Model, the --allow-net flag must be passed to allow QUIC network operations. Without it, calling quic.connect() or quic.listen() will throw an ERR_ACCESS_DENIED error.

$ node --permission --allow-fs-read=* --experimental-quic index.mjs
Error: Access to this API has been restricted. Use --allow-net to manage permissions.
  code: 'ERR_ACCESS_DENIED',
  permission: 'Net',
}

Creating a QuicEndpoint instance without connecting or listening is permitted even without --allow-net, since no network I/O occurs until quic.connect() or quic.listen() is called.

quic.connect(address[, options])

Initiate a new client-side session.

import { connect } from 'node:quic';
import { Buffer } from 'node:buffer';

const enc = new TextEncoder();
const alpn = 'foo';
const client = await connect('123.123.123.123:8888', { alpn });
await client.createUnidirectionalStream({
  body: enc.encode('hello world'),
});

By default, every call to connect(...) will create a new local QuicEndpoint instance bound to a new random local IP port. To specify the exact local address to use, or to multiplex multiple QUIC sessions over a single local port, pass the endpoint option with either a QuicEndpoint or EndpointOptions as the argument.

import { QuicEndpoint, connect } from 'node:quic';

const endpoint = new QuicEndpoint({
  address: '127.0.0.1:1234',
});

const client = await connect('123.123.123.123:8888', { endpoint });

quic.listen(onsession[, options])

Configures the endpoint to listen as a server. When a new session is initiated by a remote peer, the given onsession callback will be invoked with the created session.

import { listen } from 'node:quic';

const endpoint = await listen((session) => {
  // ... handle the session
});

// Closing the endpoint allows any sessions open when close is called
// to complete naturally while preventing new sessions from being
// initiated. Once all existing sessions have finished, the endpoint
// will be destroyed. The call returns a promise that is resolved once
// the endpoint is destroyed.
await endpoint.close();

By default, every call to listen(...) will create a new local QuicEndpoint instance bound to a new random local IP port. To specify the exact local address to use, or to multiplex multiple QUIC sessions over a single local port, pass the endpoint option with either a QuicEndpoint or EndpointOptions as the argument.

At most, any single QuicEndpoint can only be configured to listen as a server once.

quic.listEndpoints([options])

Returns the list of all QuicEndpoint instances. By default, only active endpoints are returned.

quic.constants

An object containing commonly used constants for QUIC configuration.

quic.constants.cc

Congestion control algorithm identifiers, for use with the sessionOptions.cc option:

quic.constants.DEFAULT_CIPHERS

The default TLS 1.3 cipher suite list used when sessionOptions.ciphers is not specified.

quic.constants.DEFAULT_GROUPS

The default TLS 1.3 key-exchange group list used when sessionOptions.groups is not specified.

Class: QuicEndpoint

A QuicEndpoint encapsulates the local UDP-port binding for QUIC. It can be used as both a client and a server.

new QuicEndpoint([options])

endpoint.address

The local UDP socket address to which the endpoint is bound, if any.

If the endpoint is not currently bound then the value will be undefined. Read only.

endpoint.busy

When endpoint.busy is set to true, the endpoint will temporarily reject new sessions from being created. Read/write.

// Mark the endpoint busy. New sessions will be prevented.
endpoint.busy = true;

// Mark the endpoint free. New session will be allowed.
endpoint.busy = false;

The busy property is useful when the endpoint is under heavy load and needs to temporarily reject new sessions while it catches up.

endpoint.close()

Gracefully close the endpoint. The endpoint will close and destroy itself when all currently open sessions close. Once called, new sessions will be rejected.

Returns a promise that is fulfilled when the endpoint is destroyed.

endpoint.closed

A promise that is fulfilled when the endpoint is destroyed. This will be the same promise that is returned by the endpoint.close() function. Read only.

endpoint.closing

True if endpoint.close() has been called and closing the endpoint has not yet completed. Read only.

endpoint.destroy([error])

Forcefully closes the endpoint by forcing all open sessions to be immediately closed.

endpoint.destroyed

True if endpoint.destroy() has been called. Read only.

endpoint.listening

True if the endpoint is actively listening for incoming connections. Read only.

endpoint.maxConnectionsPerHost

The maximum number of concurrent connections allowed per remote IP address. 0 means unlimited (default). Can be set at construction time via the maxConnectionsPerHost option and changed dynamically at any time. The valid range is 0 to 65535.

endpoint.maxConnectionsTotal

The maximum total number of concurrent connections across all remote addresses. 0 means unlimited (default). Can be set at construction time via the maxConnectionsTotal option and changed dynamically at any time. The valid range is 0 to 65535.

endpoint.setSNIContexts(entries[, options])

Replaces or updates the SNI TLS contexts for this endpoint. This allows changing the TLS identity (key/certificate) used for specific host names without restarting the endpoint. Existing sessions are unaffected — only new sessions will use the updated contexts.

endpoint.setSNIContexts({
  'api.example.com': { keys: [newApiKey], certs: [newApiCert] },
});

// Replace the entire SNI map
endpoint.setSNIContexts({
  'api.example.com': { keys: [newApiKey], certs: [newApiCert] },
}, { replace: true });

endpoint.stats

The statistics collected for an active endpoint. Read only.

endpoint[Symbol.asyncDispose]()

Calls endpoint.close() and returns a promise that fulfills when the endpoint has closed.

Class: QuicEndpoint.Stats

A view of the collected statistics for an endpoint.

endpointStats.createdAt

endpointStats.destroyedAt

endpointStats.bytesReceived

endpointStats.bytesSent

endpointStats.packetsReceived

endpointStats.packetsSent

endpointStats.serverSessions

endpointStats.clientSessions

endpointStats.serverBusyCount

endpointStats.retryCount

endpointStats.retryRateLimited

endpointStats.versionNegotiationCount

endpointStats.versionNegotiationRateLimited

endpointStats.statelessResetCount

endpointStats.statelessResetRateLimited

endpointStats.immediateCloseCount

endpointStats.immediateCloseRateLimited

endpointStats.sessionCreationRateLimited

endpointStats.packetsBlocked

Class: QuicSession

A QuicSession represents the local side of a QUIC connection.

session.applicationOptions

The current application-level options for this session. These include settings that are specific to the negotiated application protocol (e.g. HTTP/3) and may be negotiated separately from the transport parameters. Read only. You can use the callback session.onapplication to be informed, when settings from the remote arrive.

session.close([options])

Initiate a graceful close of the session. Existing streams will be allowed to complete but no new streams will be opened. Once all streams have closed, the session will be destroyed. The returned promise will be fulfilled once the session has been destroyed. If a non-zero code is specified, the promise will reject with an ERR_QUIC_TRANSPORT_ERROR or ERR_QUIC_APPLICATION_ERROR depending on the type.

session.opened

A promise that is fulfilled once the TLS handshake completes successfully. The resolved value contains information about the established session including the negotiated protocol, cipher suite, certificate validation status, and 0-RTT early data status.

If the handshake fails or the session is destroyed before the handshake completes, the promise will be rejected.

session.closed

A promise that is fulfilled once the session is destroyed.

session.closing

True if session.close() has been called and the session has not yet been destroyed. Read only.

session.destroy([error[, options]])

Immediately destroy the session. All streams will be destroyed and the session will be closed. If error is provided and session.onerror is set, the onerror callback is invoked before destruction. The session.closed promise will reject with the error. If options is provided, the CONNECTION_CLOSE frame sent to the peer will include the specified error code, type, and reason.

session.destroyed

True if session.destroy() has been called. Read only.

session.localTransportParams

The transport parameters advertised by the local endpoint during the handshake. Returns null if the session has been destroyed. Read only.

session.endpoint

The endpoint that created this session. Returns null if the session has been destroyed. Read only.

session.onapplication

The callback to invoke when new application options, e.g. HTTP/3 settings arrived.

session.onerror

An optional callback invoked when the session is destroyed with an error. This includes errors caused by user callbacks that throw or reject (see Callback error handling). The callback receives a single argument: the error that triggered the destruction. If the onerror callback itself throws or returns a promise that rejects, the error is surfaced as an uncaught exception. Read/write.

Can also be set via the onerror option in quic.connect() or quic.listen().

session.onstream

The callback to invoke when a new stream is initiated by a remote peer. Read/write.

If no onstream callback is set and the stream has no other consumer, an incoming stream is destroyed on arrival and a warning is emitted. An onheaders callback counts as a consumer when the negotiated application protocol supports it (e.g. HTTP/3), because it is invoked for every incoming request stream. Other stream-level callbacks (ontrailers, oninfo, onwanttrailers) do not, since they are conditional or outbound-only and would leave the stream unobservable. An HTTP/3 server that handles requests entirely through onheaders does not need to set onstream.

session.ondatagram

The callback to invoke when a new datagram is received from a remote peer. Read/write.

session.ondatagramstatus

The callback to invoke when the status of a datagram is updated. Read/write.

session.onearlyrejected

The callback to invoke when the server rejects 0-RTT early data. When this fires, all streams that were opened during the 0-RTT phase have been destroyed. The application should re-open streams if needed. Read/write.

This callback only fires on the client side when the server rejects the client’s 0-RTT attempt. The connection falls back to 1-RTT and continues normally.

session.onpathvalidation

The callback to invoke when the path validation is updated. Read/write.

session.onsessionticket

The callback to invoke when a new session ticket is received. Read/write.

session.onversionnegotiation

The callback to invoke when a version negotiation is initiated. Read/write.

session.onhandshake

The callback to invoke when the TLS handshake is completed. Read/write.

session.onnewtoken

The callback to invoke when a NEW_TOKEN token is received from the server. The token can be passed as the token option on a future connection to the same server to skip address validation. Read/write.

session.onorigin

The callback to invoke when an ORIGIN frame (RFC 9412) is received from the server, indicating which origins the server is authoritative for. Read/write.

session.ongoaway

The callback to invoke when the peer sends an HTTP/3 GOAWAY frame, indicating it is initiating a graceful shutdown. The callback receives (lastStreamId) where lastStreamId is a {bigint}:

After GOAWAY is received, session.createBidirectionalStream() will throw ERR_INVALID_STATE. Existing streams continue until they complete or the session closes.

This callback is only relevant for HTTP/3 sessions. Read/write.

session.onkeylog

The callback to invoke when TLS key material is available. Requires sessionOptions.keylog to be true. Each invocation receives a single line of NSS Key Log Format text (including a trailing newline). This is useful for decrypting packet captures with tools like Wireshark. Read/write.

Can also be set via the onkeylog option in quic.connect() or quic.listen().

session.onqlog

The callback to invoke when qlog data is available. Requires sessionOptions.qlog to be true. The callback receives a string chunk of JSON-SEQ formatted qlog data and a boolean fin flag. When fin is true, the chunk is the final qlog output for this session and the concatenated chunks form a complete qlog trace. Read/write.

Qlog data arrives during the connection lifecycle. The first chunk contains the qlog header with format metadata. Subsequent chunks contain trace events. The final chunk (with fin set to true) is emitted during session destruction and completes the JSON-SEQ output.

Can also be set via the onqlog option in quic.connect() or quic.listen().

session.createBidirectionalStream([options])

Open a new bidirectional stream. If the body option is not specified, the stream’s outgoing side remains writable and no FIN is sent immediately. The priority and incremental options are only used when the session supports priority (e.g. HTTP/3). The headers, onheaders, ontrailers, oninfo, and onwanttrailers options are only used when the session supports headers (e.g. HTTP/3).

session.createUnidirectionalStream([options])

Open a new unidirectional stream. If the body option is not specified, the stream’s outgoing side remains writable and no FIN is sent immediately. The priority and incremental options are only used when the session supports priority (e.g. HTTP/3).

session.path

The local and remote socket addresses associated with the session. Read only.

session.remoteTransportParams

The transport parameters advertised by the remote peer during the handshake. Returns null if the session has been destroyed, undefined if the handshake has not yet completed and the remote parameters are not yet available. Read only.

session.sendDatagram(datagram[, encoding])

Sends an unreliable datagram to the remote peer, returning a promise for the datagram ID.

If datagram is a string, it will be encoded using the specified encoding.

If datagram is an ArrayBufferView, the bytes are copied into an internal buffer; the caller’s source buffer is unchanged and may be reused or mutated immediately after the call returns. Callers that want to ensure their source cannot be mutated after the call (for example, when handing the buffer off to another async consumer) can call ArrayBuffer.prototype.transfer() themselves before passing the buffer.

If datagram is a Promise, it will be awaited before sending. If the session closes while awaiting, 0n is returned silently (datagrams are inherently unreliable).

If the datagram payload is zero-length (empty string after encoding, detached buffer, or zero-length view), 0n is returned and no datagram is sent.

For HTTP/3 sessions, the peer must advertise SETTINGS_H3_DATAGRAM=1 (via application: { enableDatagrams: true }) for datagrams to be sent. If the peer’s setting is 0, sendDatagram() returns 0n (per RFC 9297 §3, an endpoint MUST NOT send HTTP Datagrams unless the peer indicated support).

Datagrams cannot be fragmented — each must fit within a single QUIC packet. The maximum datagram size is determined by the peer’s maxDatagramFrameSize transport parameter (which the peer advertises during the handshake). If the peer sets this to 0, datagrams are not supported and 0n will be returned. If the datagram exceeds the peer’s limit, it will be silently dropped and 0n returned. The local maxDatagramFrameSize transport parameter (default: 1200 bytes) controls what this endpoint advertises to the peer as its own maximum.

session.servername

The SNI (Server Name Indication) host name associated with the session. This is null before the client hello is processed. Once the hello has been processed, this is either the host name string or false if the handshake had no SNI.

session.alpnProtocol

The negotiated ALPN protocol. This is null before the client hello is processed. Once ALPN has been negotiated, this is the protocol string. ALPN is mandatory in QUIC so this is never false on successful connections, unlike node:tls where this is optional.

session.certificate

The local certificate as a crypto.X509Certificate instance. Server sessions return the certificate configured for the negotiated SNI host. Client sessions return undefined unless a client certificate was sent. Returns undefined if the session is destroyed.

session.peerCertificate

The peer’s certificate as a crypto.X509Certificate instance. Returns undefined if the peer did not present a certificate or the session is destroyed.

session.ephemeralKeyInfo

The ephemeral key information for the session, with properties such as type, name, and size. Only available on client sessions. Returns undefined for server sessions or if the session is destroyed.

session.maxDatagramSize

The maximum datagram payload size in bytes that the peer will accept. This is derived from the peer’s maxDatagramFrameSize transport parameter minus the DATAGRAM frame overhead (type byte and variable-length integer encoding). Returns 0 if the peer does not support datagrams or if the handshake has not yet completed. Datagrams larger than this value will not be sent.

session.maxPendingDatagrams

The maximum number of datagrams that can be queued for sending. Datagrams are queued when sendDatagram() is called and sent opportunistically alongside stream data by the packet serialization loop. When the queue is full, the sessionOptions.datagramDropPolicy determines whether the oldest or newest datagram is dropped. Dropped datagrams are reported as lost via the ondatagramstatus callback.

This property can be changed dynamically to adjust queue capacity based on application activity or memory pressure. The valid range is 0 to 65535.

session.stats

Return the current statistics for the session. Read only.

session.updateKey()

Initiate a key update for the session.

session[Symbol.asyncDispose]()

Calls session.close() and returns a promise that fulfills when the session has closed.

Class: QuicSession.Stats

sessionStats.createdAt

sessionStats.closingAt

sessionStats.handshakeCompletedAt

sessionStats.handshakeConfirmedAt

sessionStats.bytesReceived

sessionStats.bytesSent

sessionStats.bidiInStreamCount

sessionStats.bidiOutStreamCount

sessionStats.uniInStreamCount

sessionStats.uniOutStreamCount

sessionStats.maxBytesInFlight

sessionStats.bytesInFlight

sessionStats.blockCount

sessionStats.cwnd

sessionStats.latestRtt

sessionStats.minRtt

sessionStats.rttVar

sessionStats.smoothedRtt

sessionStats.ssthresh

sessionStats.datagramsReceived

sessionStats.datagramsSent

sessionStats.datagramsAcknowledged

sessionStats.datagramsLost

sessionStats.streamsIdleTimedOut

Class: QuicError

Stability: 1 - Experimental

A QuicError is an Error subclass that carries an explicit numeric QUIC error code. Use it to abort a QUIC stream or session with a specific application-protocol-defined error code rather than letting the implementation pick a generic fallback.

The class is exported from node:quic:

import { QuicError } from 'node:quic';
const { QuicError } = require('node:quic');

When a QuicError is supplied to APIs that emit a wire frame (writer.fail(), stream.destroy()), the QUIC stack uses error.errorCode as the wire code for the resulting frame. When any other value is supplied (for example a plain Error), the implementation falls back to the negotiated application protocol’s “internal error” code (H3_INTERNAL_ERROR (0x102) for HTTP/3, or the QUIC transport-layer INTERNAL_ERROR (0x1) for raw QUIC).

The Node.js error code (error.code) defaults to 'ERR_QUIC_STREAM_ABORTED'. Callers who need a more specific code string can override it via options.code — the numeric QUIC code is unaffected.

The Node.js error code is fixed at 'ERR_QUIC_STREAM_ABORTED' so that catch blocks can distinguish a QuicError from other Node.js errors without checking the prototype chain. The numeric QUIC code lives on the separate error.errorCode property to avoid colliding with the Node.js convention that error.code is a string.

new QuicError(message, options)

import { QuicError } from 'node:quic';

const err = new QuicError('rejecting stream', { errorCode: 0x10cn });
console.log(err.code);       // 'ERR_QUIC_STREAM_ABORTED'
console.log(err.errorCode);  // 268n
console.log(err.type);       // 'application'

const custom = new QuicError('custom failure', {
  errorCode: 0x10cn,
  code: 'ERR_MY_QUIC_FAILURE',
});
console.log(custom.code);    // 'ERR_MY_QUIC_FAILURE'

error.errorCode

The numeric QUIC error code carried by this error.

error.type

Either 'application' or 'transport'. Indicates the namespace of error.errorCode.

Class: QuicStream

stream.opened

A promise that is immediately fulfilled, if the stream fits within flow control limits or fulfilled when the pending stream is created. It rejects, if a pending stream is closed with an error before being created.

stream.closed

A promise that is fulfilled when the stream is fully closed. It resolves when the stream closes cleanly (including idle timeout). It rejects with an ERR_QUIC_APPLICATION_ERROR or ERR_QUIC_TRANSPORT_ERROR when the stream is closed due to a QUIC error (e.g., stream reset by the peer, CONNECTION_CLOSE with a non-zero error code).

stream.destroy([error[, options]])

Immediately and abruptly destroys the stream. If error is provided and stream.onerror is set, the onerror callback is invoked before destruction. The stream.closed promise rejects with the error.

Buffered incoming data remains readable after destruction, even if iteration has not started. If the readable side ended without a FIN, the iterator yields that data before reporting the truncation or error according to sessionOptions.truncatedReads.

When the stream is destroyed with an error (or with an explicit options.code), the QUIC stack signals the abort to the peer:

Both frames carry the same wire code, resolved with the following precedence:

  1. options.code, when explicitly provided.
  2. error.errorCode, when error is a QuicError.
  3. The negotiated application protocol’s “internal error” code (H3_INTERNAL_ERROR (0x102) for HTTP/3, or the QUIC transport-layer INTERNAL_ERROR (0x1) for raw QUIC).

A clean destroy — no error and no options.code — does not emit RESET_STREAM or STOP_SENDING; the stream’s existing close machinery handles teardown.

See Aborting a stream for an overview of the available stream-abort APIs.

stream.destroyed

True if stream.destroy() has been called.

Aborting a stream

A QuicStream can be aborted in several ways, each producing different wire-frame side effects:

When error is a QuicError, its error.errorCode is used as the wire code for both writer.fail() and stream.destroy(). Otherwise the implementation falls back to the negotiated application protocol’s “internal error” code (see QuicError).

stream.stopSending() and stream.resetStream() do not perform this derivation: they send code as given.

stream.resetStream([code])

Tells the peer that this end will not send any more data on this stream, sending a RESET_STREAM frame carrying code. The readable side is left open, so data already sent by the peer remains available to read.

Any data still queued for sending is discarded. A reset stream is never acknowledged by the peer, so the outbound queue can no longer drain.

No acknowledgement of this action is provided. The call does nothing if the stream has been destroyed, if it has already been reset, or if it is a remote-initiated unidirectional stream, which has no writable side to abort.

stream.stopSending([code])

Asks the peer to stop sending data on this stream, sending a STOP_SENDING frame carrying code. The writable side is left open, so this end can still send data.

No acknowledgement of this action is provided. The call does nothing if the stream has been destroyed, or if it is a locally-initiated unidirectional stream, which has no readable side to abort.

stream.early

True if any data on this stream was received as 0-RTT (early data) before the TLS handshake completed. Early data is less secure and could potentially be replayed by an attacker. Applications should treat early data with appropriate caution.

This property is only meaningful on the server side. On the client side, it is always false.

stream.direction

The directionality of the stream. Read only.

stream.budget

The maximum number of bytes that the writer will buffer before writeSync() returns false. When the buffered data exceeds this limit, the caller should wait for drain before writing more.

The value can be changed dynamically at any time. This is particularly useful for streams received via the onstream callback, where the default (65536) may need to be adjusted based on application needs. The valid range is 0 to 4294967295.

stream.id

The stream ID, or null while the stream is pending. Read only.

stream.onerror

An optional callback invoked when the stream is destroyed with an error. This includes errors caused by user callbacks that throw or reject (see Callback error handling). The callback receives a single argument: the error that triggered the destruction. If the onerror callback itself throws or returns a promise that rejects, the error is surfaced as an uncaught exception. Read/write.

stream.onblocked

The callback to invoke when the stream is blocked. Read/write.

stream.onreset

The callback to invoke when the peer aborts a direction of the stream by sending a RESET_STREAM frame (the peer abandons their writable side, so no further data will arrive on our readable side).

The callback receives a Node.js error whose errorCode (bigint) property carries the application error code from the wire frame.

The stream is not automatically destroyed when this callback fires — the application chooses how to react. Common patterns are: ignore (and continue using the still-active direction on a bidirectional stream), abort the other direction with writer.fail(), or tear down the whole stream with stream.destroy(). Read/write.

stream.onstopsending

The callback to invoke when the peer aborts a direction of the stream by sending a STOP_SENDING frame (the peer asks us to stop writing on our writable side).

The callback receives a Node.js error whose errorCode (bigint) property carries the application error code from the wire frame. Read/write.

stream.headers

The buffered initial headers received on this stream, or undefined if the application does not support headers or no headers have been received yet. For server-side streams, this contains the request headers (e.g., :method, :path, :scheme). For client-side streams, this contains the response headers (e.g., :status).

Header names are lowercase strings. Multi-value headers are represented as arrays. The object has __proto__: null.

stream.onheaders

The callback to invoke when initial headers are received on the stream. The callback receives (headers) where headers is an object (same format as stream.headers). For HTTP/3, this delivers request pseudo-headers on the server side and response headers on the client side. Throws ERR_INVALID_STATE if set on a session that does not support headers. Read/write.

stream.ontrailers

The callback to invoke when trailing headers are received from the peer. The callback receives (trailers) where trailers is an object in the same format as stream.headers. Throws ERR_INVALID_STATE if set on a session that does not support headers. Read/write.

stream.oninfo

The callback to invoke when informational (1xx) headers are received from the server. The callback receives (headers) where headers is an object in the same format as stream.headers. Informational headers are sent before the final response (e.g., 103 Early Hints). Throws ERR_INVALID_STATE if set on a session that does not support headers. Read/write.

stream.onwanttrailers

The callback to invoke when the application is ready for trailing headers to be sent. This is called synchronously — the user must call stream.sendTrailers() within this callback. Throws ERR_INVALID_STATE if set on a session that does not support headers. Read/write.

stream.pendingTrailers

Set trailing headers to be sent automatically when the application requests them. This is an alternative to the stream.onwanttrailers callback for cases where the trailers are known before the body completes. Throws ERR_INVALID_STATE if set on a session that does not support headers. Read/write.

stream.sendHeaders(headers[, options])

Sends initial or response headers on the stream. For client-side streams, this sends request headers. For server-side streams, this sends response headers. Throws ERR_INVALID_STATE if the session does not support headers.

stream.sendInformationalHeaders(headers)

Sends informational (1xx) response headers. Server only. Throws ERR_INVALID_STATE if the session does not support headers.

stream.sendTrailers(headers)

Sends trailing headers on the stream. Must be called synchronously during the stream.onwanttrailers callback, or set ahead of time via stream.pendingTrailers. Throws ERR_INVALID_STATE if the session does not support headers.

stream.priority

The current priority of the stream. Returns null if the session does not support priority (e.g. non-HTTP/3) or if the stream has been destroyed. Read only. Use stream.setPriority() to change the priority.

On client-side HTTP/3 sessions, the value reflects what was set via stream.setPriority(). On server-side HTTP/3 sessions, the value reflects the peer’s requested priority (e.g., from PRIORITY_UPDATE frames).

stream.setPriority([options])

Sets the priority of the stream. Throws ERR_INVALID_STATE if the session does not support priority (e.g. non-HTTP/3). Has no effect if the stream has been destroyed.

stream[Symbol.asyncIterator]()

The stream implements Symbol.asyncIterator, making it directly usable in for await...of loops. Each iteration yields a batch of Uint8Array chunks.

Only one async iterator can be obtained per stream. A second call throws ERR_INVALID_STATE. Non-readable streams (outbound-only unidirectional or closed) return an immediately-finished iterator.

for await (const chunks of stream) {
  for (const chunk of chunks) {
    // Process each Uint8Array chunk
  }
}

Compatible with stream/iter utilities:

import Stream from 'node:stream/iter';
const body = await Stream.bytes(stream);
const text = await Stream.text(stream);
await Stream.pipeTo(stream, someWriter);

stream.writer

Returns a Writer object for pushing data to the stream incrementally. The Writer implements the stream/iter Writer interface with the try-sync-fallback-to-async pattern.

Only available when no body source was provided at creation time or via stream.setBody(). Non-writable streams return an already-closed Writer. Throws ERR_INVALID_STATE if the outbound is already configured.

The Writer has the following methods:

import { ondrain } from 'node:stream/iter';

while (!writer.writeSync(chunk)) {
  const drain = ondrain(writer);
  if (drain === null) break;
  await drain;
}

The bytes from each writeSync() / writevSync() / write() / writev() input chunk are copied into an internal buffer, so the caller’s source buffer is unchanged and may be reused or mutated immediately after the call returns. Callers that want to ensure a source buffer cannot be mutated after handing it off can call ArrayBuffer.prototype.transfer() themselves before passing the buffer.

stream.setBody(body)

Sets the outbound body source for the stream. Can only be called once. Mutually exclusive with stream.writer.

The following body source types are supported:

Throws ERR_INVALID_STATE if the outbound is already configured or if the writer has been accessed.

stream.session

The session that created this stream, or null if the stream has been destroyed. Read only.

stream.stats

The current statistics for the stream. Read only.

Class: QuicStream.Stats

streamStats.ackedAt

streamStats.bytesAccumulated

The current number of bytes sitting in the stream’s receive accumulation buffer, awaiting delivery to the application. A value near zero indicates the reader is keeping up with incoming data. A value near the stream’s flow control window indicates the application is not consuming data fast enough.

streamStats.bytesReceived

streamStats.bytesSent

streamStats.createdAt

streamStats.destroyedAt

streamStats.finalSize

streamStats.isConnected

streamStats.maxBytesAccumulated

The peak number of bytes that were accumulated in the stream’s receive buffer at any point during the stream’s lifetime. This value only increases monotonically. It is useful for diagnosing whether a stream experienced backpressure episodes and whether the accumulation buffer sizing is appropriate for the workload.

streamStats.maxOffset

streamStats.maxOffsetAcknowledged

streamStats.maxOffsetReceived

streamStats.openedAt

streamStats.receivedAt

Types

type: ApplicationOptions

The application specific options.

applicationOptions.maxHeaderPairs

Maximum number of header name-value pairs accepted per header block. Headers beyond this limit are silently dropped. Default: 128

applicationOptions.maxHeaderLength

Maximum total byte length of all header names and values combined per header block. Headers that would push the total over this limit are silently dropped. Default: 8192

applicationOptions.maxFieldSectionSize

Maximum size of a compressed header field section (QPACK). 0 means unlimited. Default: 0

applicationOptions.qpackMaxDTableCapacity

QPACK dynamic table capacity in bytes. Set to 0 to disable the dynamic table. Default: 4096

applicationOptions.qpackEncoderMaxDTableCapacity

QPACK encoder maximum dynamic table capacity. Default: 4096

applicationOptions.qpackBlockedStreams

Maximum number of streams that can e blocked waiting for QPACK dynamic table updates. Default: 100

applicationOptions.enableConnectProtocol

Enable the extended CONNECT protocol (RFC 9220). Default: false

applicationOptions.enableDatagrams

Enable HTTP/3 datagrams (RFC 9297). Default: false

Type: EndpointOptions

The endpoint configuration options passed when constructing a new QuicEndpoint instance.

endpointOptions.address

If not specified the endpoint will bind to IPv4 localhost on a random port.

endpointOptions.blockList

An optional net.BlockList instance for filtering incoming packets by source address. When configured, every received UDP packet is checked against the block list before any QUIC processing occurs, minimizing resource expenditure on blocked sources. The block list is evaluated live — rules added to the BlockList object after the endpoint is created take effect immediately.

See endpointOptions.blockListPolicy for how matches are interpreted.

endpointOptions.blockListPolicy

Controls how the endpointOptions.blockList is interpreted:

If no block list is configured, this option has no effect.

endpointOptions.addressLRUSize

The endpoint maintains an internal cache of validated socket addresses as a performance optimization. This option sets the maximum number of addresses that are cached. The value must be greater than 0. This is an advanced option that users typically won’t have need to specify.

endpointOptions.disableStatelessReset

When true, the endpoint will not send stateless reset packets in response to packets from unknown connections. Stateless resets allow a peer to detect that a connection has been lost even when the server has no state for it. Disabling them may be useful in testing or when stateless resets are handled at a different layer.

endpointOptions.idleTimeout

The number of seconds an endpoint will remain alive after all sessions have closed and it is no longer listening. A value of 0 (default) means the endpoint is only destroyed when explicitly closed via endpoint.close() or endpoint.destroy(). A positive value starts an idle timer when the endpoint becomes idle; if no new sessions are created before the timer fires, the endpoint is automatically destroyed. This is useful for connection pooling where endpoints should linger briefly for reuse by future connect() calls.

endpointOptions.ipv6Only

When true, indicates that the endpoint should bind only to IPv6 addresses.

endpointOptions.reusePort

When true, allows multiple endpoints (across separate processes) to bind to the same address and port. The kernel will load-balance incoming UDP datagrams across all sockets bound with this option. This enables horizontal scaling of QUIC servers by running multiple Node.js processes on the same port.

Supported on Linux 3.9+ and DragonFlyBSD 3.6+. On unsupported platforms, the bind will fail with an error.

endpointOptions.maxConnectionsPerHost

Specifies the maximum number of concurrent sessions allowed per remote IP address (ignoring port). When the limit is reached, new connections from the same IP are refused with CONNECTION_REFUSED. A value of 0 disables the limit. The maximum value is 65535.

This limit can also be changed dynamically after construction via endpoint.maxConnectionsPerHost.

endpointOptions.maxConnectionsTotal

Specifies the maximum total number of concurrent sessions across all remote addresses. When the limit is reached, new connections are refused with CONNECTION_REFUSED. A value of 0 disables the limit. The maximum value is 65535.

This limit can also be changed dynamically after construction via endpoint.maxConnectionsTotal.

endpointOptions.retryRate

The maximum number of QUIC retry packets the endpoint will send per second. This is a global rate limit (not per-host) that caps the total server-wide retry response rate, preventing spoofed-source floods from consuming unbounded resources.

endpointOptions.retryBurst

The maximum burst of retry packets allowed before rate limiting takes effect.

endpointOptions.statelessResetRate

The maximum number of stateless reset packets the endpoint will send per second.

endpointOptions.statelessResetBurst

The maximum burst of stateless reset packets allowed before rate limiting takes effect.

endpointOptions.versionNegotiationRate

The maximum number of version negotiation packets the endpoint will send per second.

endpointOptions.versionNegotiationBurst

The maximum burst of version negotiation packets allowed before rate limiting takes effect.

endpointOptions.immediateCloseRate

The maximum number of immediate connection close packets the endpoint will send per second.

endpointOptions.immediateCloseBurst

The maximum burst of immediate connection close packets allowed before rate limiting takes effect.

endpointOptions.sessionCreationRate

The maximum number of new sessions that a single remote address can create per second. This is a per-host rate limit tracked in the address validation LRU cache. It prevents a validated remote address from churning through sessions (rapidly opening and abandoning connections) faster than the server can handle. For benchmarking where traffic comes from a single source, set this to a high value.

endpointOptions.sessionCreationBurst

The maximum burst of new session creations allowed from a single remote address before rate limiting takes effect.

endpointOptions.retryTokenExpiration

Specifies the length of time a QUIC retry token is considered valid.

endpointOptions.resetTokenSecret

Specifies the 16-byte secret used to generate QUIC retry tokens.

endpointOptions.tokenExpiration

Specifies the length of time a QUIC token is considered valid.

endpointOptions.tokenSecret

Specifies the 16-byte secret used to generate QUIC tokens.

endpointOptions.udpReceiveBufferSize

endpointOptions.udpSendBufferSize

endpointOptions.udpTTL

endpointOptions.validateAddress

When true, requires that the endpoint validate peer addresses using retry packets while establishing a new connection.

Type: SessionOptions

sessionOptions.alpn

The ALPN (Application-Layer Protocol Negotiation) identifier(s).

For client sessions, this is a single string specifying the protocol the client wants to use (e.g. 'h3').

For server sessions, this is a non-empty array of protocol names in preference order that the server supports (e.g. ['h3', 'h3-29']). During the TLS handshake, the server selects the first protocol from its list that the client also supports.

The negotiated ALPN determines which Application implementation is used for the session. 'h3' and 'h3-*' variants select the HTTP/3 application; all other values select the default application.

Default: 'h3'

sessionOptions.application

Application-specific options.

const { listen } = await import('node:quic');

await listen((session) => { /* ... */ }, {
  application: {
    maxHeaderPairs: 64,
    qpackMaxDTableCapacity: 8192,
    enableDatagrams: true,
  },
  // ... other session options
});

sessionOptions.ca

The CA certificates to use for sessions.

sessionOptions.cc

Specifies the congestion control algorithm that will be used. Must be set to one of either 'reno', 'cubic', or 'bbr'.

This is an advanced option that users typically won’t have need to specify.

sessionOptions.certs (client only)

The TLS certificates to use for client sessions. For server sessions, certificates are specified per-identity in the sessionOptions.sni map.

sessionOptions.certificateCompression

Enables TLS certificate compression (RFC 8879) for this session. When omitted, certificate compression is disabled.

On the server side, the certificate chain is compressed using the first listed algorithm that the client advertises support for. On the client side, the listed algorithms are advertised to the server so that the server may compress its certificate. When client authentication is in use, the option also controls compression of the client’s certificate.

Compressing the certificate chain is especially useful for QUIC because it reduces the size of the server’s first flight, which is bounded by the anti-amplification limit (see Certificate size and handshake performance). Certificate compression requires TLS 1.3, which QUIC always uses.

At most three algorithms may be specified. The option is silently ignored if Node.js was built against a shared OpenSSL that lacks certificate compression support.

sessionOptions.ciphers

The list of supported TLS 1.3 cipher algorithms.

sessionOptions.crl

The CRL to use for sessions.

sessionOptions.enableEarlyData

When true, enables TLS 0-RTT early data for this session. Early data allows the client to send application data before the TLS handshake completes, reducing latency on reconnection when a valid session ticket is available. Set to false to disable early data support.

sessionOptions.groups

The list of supported TLS 1.3 cipher groups.

sessionOptions.keylog

When true, enables TLS key logging for the session. Key material is delivered to the session.onkeylog callback in NSS Key Log Format. Each callback invocation receives a single line of key material. The output can be used with tools such as Wireshark to decrypt captured QUIC traffic.

sessionOptions.keys (client only)

The TLS crypto keys to use for client sessions. For server sessions, keys are specified per-identity in the sessionOptions.sni map.

sessionOptions.maxPayloadSize

Specifies the maximum UDP packet payload size.

sessionOptions.maxStreamWindow

Specifies the maximum stream flow-control window size.

sessionOptions.maxWindow

Specifies the maximum session flow-control window size.

sessionOptions.minVersion

The minimum QUIC version number to allow. This is an advanced option that users typically won’t have need to specify.

sessionOptions.preferredAddressPolicy

When the remote peer advertises a preferred address, this option specifies whether to use it or ignore it. The default is 'ignore' because honoring a server’s preferred address causes the client to migrate its connection to a different IP address, which can be exploited for data exfiltration attacks that are indistinguishable from legitimate QUIC connection migration at the network level. Set to 'use' only when connecting to trusted servers that require preferred address migration.

sessionOptions.qlog

When true, enables qlog diagnostic output for the session. Qlog data is delivered to the session.onqlog callback as chunks of JSON-SEQ formatted text. The output can be analyzed with qlog visualization tools such as qvis.

sessionOptions.sessionTicket

sessionOptions.datagramDropPolicy

Controls which datagram to drop when the pending datagram queue (sized by session.maxPendingDatagrams) is full. Must be one of 'drop-oldest' (discard the oldest queued datagram to make room) or 'drop-newest' (reject the incoming datagram). Dropped datagrams are reported as lost via the ondatagramstatus callback.

This option is immutable after session creation.

sessionOptions.streamIdleTimeout

The maximum time in milliseconds that a peer-initiated stream can be idle (no data received) before it is automatically destroyed. This protects against slowloris-style attacks where a remote peer opens streams but never sends data, holding server resources indefinitely. Only peer-initiated streams are checked — locally-initiated streams are the application’s responsibility. Set to 0 to disable.

The idle check runs as part of the normal send processing loop, so it adds no additional timers or event loop overhead. The session.stats.streamsIdleTimedOut counter tracks how many streams have been destroyed by this mechanism.

sessionOptions.maxDatagramSendAttempts

The maximum number of SendPendingData cycles a datagram can survive without being sent before it is abandoned. When a datagram cannot be sent due to congestion control or packet size constraints, it remains in the queue and the attempt counter increments. Once the limit is reached, the datagram is dropped and reported as 'abandoned' via the ondatagramstatus callback. Valid range: 1 to 255.

sessionOptions.drainingPeriodMultiplier

A multiplier applied to the Probe Timeout (PTO) to compute the draining period duration after receiving a CONNECTION_CLOSE frame from the peer. RFC 9000 Section 10.2 requires the draining period to persist for at least three times the current PTO. The valid range is 3 to 255. Values below 3 are clamped to 3.

sessionOptions.handshakeTimeout

Specifies the maximum number of milliseconds a TLS handshake is permitted to take to complete before timing out.

sessionOptions.initialRtt

Specifies the initial round-trip time estimate in milliseconds. This value is used for probe timeout (PTO) computation, initial pacing, and early loss detection before the first actual RTT sample is collected from the connection. The default of 333ms is appropriate for the general internet. For low-latency environments such as loopback or same-rack deployments, setting a value closer to the actual RTT (e.g., 1) avoids unnecessarily conservative initial behavior.

sessionOptions.keepAlive

Specifies the keep-alive timeout in milliseconds. When set to a non-zero value, PING frames will be sent automatically to keep the connection alive before the idle timeout fires. The value should be less than the effective idle timeout (maxIdleTimeout transport parameter) to be useful.

sessionOptions.truncatedReads

Controls how reading a stream reports a truncated read. A stream’s read side can end without receiving a QUIC FIN, meaning the peer never signalled that the whole stream had been sent and the data received may be incomplete. This selects how the stream’s async iterator reports this:

sessionOptions.verifyPeer (client only)

Controls how the client handles server certificate validation:

sessionOptions.servername (client only)

The peer server name to target (SNI). Defaults to 'localhost'.

sessionOptions.sni (server only)

An object mapping host names to TLS identity options for Server Name Indication (SNI) support. This is required for server sessions and must contain at least one entry. The special key '*' specifies the optional default/fallback identity used when no other host name matches. If no wildcard entry is provided, connections with unrecognized server names will be rejected with a TLS unrecognized_name alert. Each entry may contain:

const endpoint = await listen(callback, {
  sni: {
    '*': { keys: [defaultKey], certs: [defaultCert] },
    'api.example.com': { keys: [apiKey], certs: [apiCert], port: 8443 },
    'www.example.com': { keys: [wwwKey], certs: [wwwCert] },
    'internal.example.com': { keys: [intKey], certs: [intCert], authoritative: false },
  },
});

Shared TLS options (such as ciphers, groups, keylog, and verifyClient) are specified at the top level of the session options and apply to all identities. Each SNI entry overrides only the per-identity certificate fields.

The SNI map can be replaced at runtime using endpoint.setSNIContexts(), which atomically swaps the map for new sessions while existing sessions continue to use their original identity.

sessionOptions.tlsTrace

True to enable TLS tracing output.

sessionOptions.token (client only)

An opaque address validation token previously received from the server via the session.onnewtoken callback. Providing a valid token on reconnection allows the client to skip the server’s address validation, reducing handshake latency.

sessionOptions.transportParams

The QUIC transport parameters to use for the session.

sessionOptions.unacknowledgedPacketThreshold

Specifies the maximum number of unacknowledged packets a session should allow.

sessionOptions.rejectUnauthorized

If true, the peer certificate is verified against the list of supplied CAs. An error is emitted if verification fails; the error can be inspected via the validationErrorReason and validationErrorCode fields in the handshake callback. If false, peer certificate verification errors are ignored.

sessionOptions.reuseEndpoint

When true (the default), connect() will attempt to reuse an existing endpoint rather than creating a new one for each session. This provides connection pooling behavior — multiple sessions can share a single UDP socket. The reuse logic will not return an endpoint that is listening on the same address as the connect target (to prevent CID routing conflicts).

Set to false to force creation of a new endpoint for the session. This is useful when endpoint isolation is required (e.g., testing stateless reset behavior where source port identity matters).

sessionOptions.verifyClient

True to require verification of TLS client certificate.

sessionOptions.verifyPrivateKey (client only)

True to require private key verification for client sessions. For server sessions, this option is specified per-identity in the sessionOptions.sni map.

sessionOptions.version

The QUIC version number to use. This is an advanced option that users typically won’t have need to specify.

Type: TransportParams

The TransportParams type represents the QUIC transport parameters that are negotiated during session establishment. These parameters are used when creating a session. The negotiated values can be observed via the session.localTransportParams and session.remoteTransportParams properties.

transportParams.initialSCID

The initial source connection ID (SCID) specified. This field is ignored on creation of the session and is provided for informational purposes only when available in the session.localTransportParams and session.remoteTransportParams properties.

transportParams.originalDCID

The original destination connection ID (DCID) specified. This field is ignored on creation of the session and is provided for informational purposes only when available in the session.localTransportParams and session.remoteTransportParams properties.

transportParams.preferredAddressIpv4

transportParams.preferredAddressIpv6

transportParams.initialMaxStreamDataBidiLocal

transportParams.initialMaxStreamDataBidiRemote

transportParams.initialMaxStreamDataUni

transportParams.initialMaxData

transportParams.initialMaxStreamsBidi

transportParams.initialMaxStreamsUni

transportParams.maxIdleTimeout

transportParams.activeConnectionIDLimit

transportParams.ackDelayExponent

transportParams.maxAckDelay

transportParams.maxDatagramFrameSize

The maximum size in bytes of a DATAGRAM frame payload that this endpoint is willing to receive. Set to 0 to disable datagram support. The peer will not send datagrams larger than this value. The actual maximum size of a datagram that can be sent is determined by the peer’s maxDatagramFrameSize, not this endpoint’s value.

transportParams.retrySCID

The retry connection ID specified. This field is ignored on creation of the session and is provided for informational purposes only when available in the session.localTransportParams and session.remoteTransportParams properties.

Callbacks

Callback error handling

All session and stream callbacks may be synchronous functions or async functions. If a callback throws synchronously or returns a promise that rejects, the error is caught and the owning session or stream is destroyed with that error:

Before destruction, the optional session.onerror or stream.onerror callback is invoked (if set), giving the application a chance to observe or log the error. The session.closed or stream.closed promise will reject with the error.

If the onerror callback itself throws or returns a promise that rejects, the error from onerror is surfaced as an uncaught exception.

Callback: OnSessionCallback

The callback function that is invoked when a new server session is initiated by a remote peer. It is called once the peer’s TLS ClientHello has been processed, so the negotiated TLS parameters are immediately available when the callback runs. Sessions whose handshake is rejected before this point are never surfaced.

Callback: OnStreamCallback

Callback: OnDatagramCallback

Callback: OnDatagramStatusCallback

Callback: OnApplicationCallback

The callback function that is invoked when application options change. E.g. for http/3 settings are included in applications options and may arrive after the connection is established.

Callback: OnPathValidationCallback

Callback: OnSessionTicketCallback

Callback: OnVersionNegotiationCallback

Called when the server responds to the client’s Initial packet with a Version Negotiation packet, indicating that the version used by the client is not supported. The session is always destroyed immediately after this callback returns.

Callback: OnHandshakeCallback

Callback: OnNewTokenCallback

Callback: OnOriginCallback

Callback: OnKeylogCallback

Called when TLS key material is available. Only fires when sessionOptions.keylog is true. Multiple lines are emitted during the TLS 1.3 handshake, each containing a secret label, the client random, and the secret value.

Callback: OnQlogCallback

Called when qlog diagnostic data is available. Only fires when sessionOptions.qlog is true. The data chunks should be concatenated in order to produce the complete qlog output. When fin is true, no more chunks will be emitted and the concatenated result is a complete JSON-SEQ document.

Callback: OnBlockedCallback

Callback: OnStreamErrorCallback

Callback: OnHeadersCallback

Called when initial request or response headers are received. For HTTP/3, this delivers request pseudo-headers on the server and response headers on the client.

Callback: OnTrailersCallback

Called when trailing headers are received from the peer.

Callback: OnInfoCallback

Called when informational (1xx) headers are received from the server (e.g., 103 Early Hints).

HTTP/3 support

When the negotiated ALPN identifier is 'h3' (or one of the 'h3-*' draft variants), the QUIC session runs the HTTP/3 application backed by nghttp3. 'h3' is the default ALPN for quic.connect() and quic.listen(), so HTTP/3 is what you get unless you select a different ALPN explicitly.

Selecting the HTTP/3 application enables a number of stream- and session-level capabilities that are not available to non-HTTP/3 applications:

Minimal HTTP/3 client

import { connect } from 'node:quic';
import process from 'node:process';

const session = await connect('example.com:443', {
  // ALPN defaults to 'h3'.
  servername: 'example.com',
});
await session.opened;

const stream = await session.createBidirectionalStream({
  headers: {
    ':method': 'GET',
    ':path': '/',
    ':scheme': 'https',
    ':authority': 'example.com',
  },
  onheaders(headers) {
    console.log('status:', headers[':status']);
  },
});

const decoder = new TextDecoder();
for await (const chunks of stream) {
  for (const chunk of chunks) {
    process.stdout.write(decoder.decode(chunk, { stream: true }));
  }
}

await session.close();

A few things to note:

Minimal HTTP/3 server

import { listen } from 'node:quic';

const encoder = new TextEncoder();

const endpoint = await listen((session) => {
  // The session.onstream callback fires for each new client-initiated
  // stream. It is optional here: with `onheaders` configured below,
  // request streams are consumed through that callback.
}, {
  sni: { '*': { keys: [defaultKey], certs: [defaultCert] } },
  // ALPN defaults to 'h3'.
  onheaders(headers) {
    // `this` is the QuicStream. Pseudo-headers are available on the
    // request header block (`:method`, `:path`, `:scheme`,
    // `:authority`).
    if (headers[':path'] === '/health') {
      this.sendHeaders({ ':status': '200', 'content-type': 'text/plain' });
      const w = this.writer;
      w.writeSync(encoder.encode('ok\n'));
      w.endSync();
    } else {
      this.sendHeaders({ ':status': '404' }, { terminal: true });
    }
  },
});

console.log('listening on', endpoint.address);

Server-side notes:

What is not implemented

Performance measurement

QUIC sessions, streams, and endpoints emit PerformanceEntry objects with entryType set to 'quic'. These entries are only created when a PerformanceObserver is observing the 'quic' entry type, ensuring zero overhead when not in use.

Each entry provides:

QuicEndpoint entries

QuicSession entries

QuicStream entries

Example

import { PerformanceObserver } from 'node:perf_hooks';

const obs = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.log(`${entry.name}: ${entry.duration.toFixed(1)}ms`);
    if (entry.name === 'QuicSession') {
      const { stats, handshake } = entry.detail;
      console.log(`  protocol: ${handshake?.protocol}`);
      console.log(`  bytes sent: ${stats.bytesSent}`);
      console.log(`  smoothed RTT: ${stats.smoothedRtt}ns`);
    }
  }
});
obs.observe({ entryTypes: ['quic'] });

Diagnostic Channels

Channel: quic.endpoint.created

Published when a new endpoint is created.

Channel: quic.endpoint.listen

Published when an endpoint begins listening for incoming connections.

Channel: quic.endpoint.connect

Published when quic.connect() is about to create a client session. Fires before the ngtcp2 connection is established, allowing diagnostic subscribers to observe the connection intent.

Channel: quic.endpoint.closing

Published when an endpoint begins gracefully closing.

Channel: quic.endpoint.closed

Published when an endpoint has finished closing and is destroyed.

Channel: quic.endpoint.error

Published when an endpoint encounters an error that causes it to close.

Channel: quic.endpoint.busy.change

Published when an endpoint’s busy state changes.

Channel: quic.session.application

Published when a locally-initiated stream is opened.

Channel: quic.session.created.client

Published when a client-initiated session is created.

Channel: quic.session.created.server

Published when a server-side session is created for an incoming connection.

Channel: quic.session.open.stream

Published when a locally-initiated stream is opened.

Channel: quic.session.received.stream

Published when a remotely-initiated stream is received.

Channel: quic.session.send.datagram

Published when a datagram is queued for sending.

Channel: quic.session.update.key

Published when a TLS key update is initiated.

Channel: quic.session.closing

Published when a session begins gracefully closing (including when a GOAWAY frame is received from the peer).

Channel: quic.session.closed

Published when a session is destroyed. The stats object is a snapshot of the final statistics at the time of destruction.

Channel: quic.session.error

Published when a session is destroyed due to an error. Fires before the onerror callback and before streams are torn down. Unlike quic.session.closed (which fires for both clean and error closes), this channel fires only when an error is present, making it suitable for error-only alerting.

Channel: quic.session.receive.datagram

Published when a datagram is received from the remote peer.

Channel: quic.session.receive.datagram.status

Published when the delivery status of a sent datagram is updated.

Channel: quic.session.path.validation

Published when a path validation attempt completes.

Channel: quic.session.new.token

Published when a client session receives a NEW_TOKEN frame from the server.

Channel: quic.session.ticket

Published when a new TLS session ticket is received.

Channel: quic.session.version.negotiation

Published when the client receives a Version Negotiation packet from the server. The session is always destroyed immediately after.

Channel: quic.session.receive.origin

Published when the session receives an ORIGIN frame (RFC 9412) from the peer.

Channel: quic.session.handshake

Published when the TLS handshake completes.

Channel: quic.session.goaway

Published when the peer sends an HTTP/3 GOAWAY frame. Streams with IDs above lastStreamId were not processed and can be retried on a new connection. A lastStreamId of -1n indicates a shutdown notice without a stream boundary.

Channel: quic.session.early.rejected

Published when the server rejects 0-RTT early data. All streams that were opened during the 0-RTT phase have been destroyed. Useful for diagnosing latency regressions when 0-RTT is expected to succeed.

Channel: quic.stream.closed

Published when a stream is destroyed. The stats object is a snapshot of the final statistics at the time of destruction.

Channel: quic.stream.headers

Published when initial headers are received on a stream. For HTTP/3 server-side streams, this contains request pseudo-headers (:method, :path, etc.). For client-side streams, this contains response headers (:status, etc.).

Channel: quic.stream.trailers

Published when trailing headers are received on a stream.

Channel: quic.stream.info

Published when informational (1xx) headers are received on a stream (e.g., 103 Early Hints).

Channel: quic.stream.reset

Published when a stream receives a RESET_STREAM frame from the peer, indicating the peer has aborted its sending direction. This is a key signal for diagnosing application-level issues such as cancelled requests.

Channel: quic.stream.blocked

Published when a stream is flow-control blocked and cannot send data until the peer increases the flow control window. Useful for diagnosing throughput issues caused by flow control.