HTTP/2 can carry several independent request-response streams through one session instead of treating each application exchange as an ordinary HTTP/1 connection pattern. Node exposes low-level HTTP/2 session and stream APIs as well as a compatibility layer. This Node 24 guide uses the low-level APIs so the new concepts are visible. The fixtures run cleartext HTTP/2, often called h2c, on loopback with a Node client. That is a controlled teaching setup. Browser-facing HTTP/2 normally needs an HTTPS deployment with valid certificates and protocol negotiation; do not assume the cleartext example is directly usable by a browser.
Separate a session from each request stream
An HTTP/2 session represents the connection-level protocol relationship between peers. A request creates a stream within that session. Several streams can be active at once, and each has its own request headers, response headers, body, and completion. Closing one request stream is therefore not the same action as closing the session. Reusing a session is the mechanism the practice demonstrates; opening a new session for every request hides much of the intended multiplexing behavior.
The server emits a stream event with the stream and request headers. It responds using stream.respond and sends body bytes through the stream. The client obtains a request stream from session.request. It listens for response headers and consumes the body before considering that request complete. A session error and a stream error are different failure scopes and both deserve handling. Keep per-request state on the stream or in a deliberate map instead of storing one current request on the shared session.
Use HTTP/2 headers and protocol rules
HTTP/2 includes pseudo-headers such as :method, :path, :scheme, :authority, and :status. The client fixture supplies method and path, while the server supplies status. These are part of the HTTP/2 protocol interface and are not ordinary custom header names. Header names are lowercase, and connection-specific HTTP/1 headers such as Connection should not be copied into HTTP/2 messages. An adapter or reverse proxy may handle differences, but a low-level implementation must follow the right protocol rules itself.
A body still has a media type and application format. HTTP/2 does not automatically validate JSON, authorize a route, or make a status code successful. If Content-Length is declared, it still needs to match the body representation. Use the stream's lifecycle rather than assuming response headers imply complete body delivery. Request trailers and advanced protocol features have additional APIs and constraints; add them only when the product needs them. The introductory fixture keeps one small text body so the basic pseudo-header and stream behavior are easy to verify.
Make TLS and compatibility prerequisites explicit
http2.createServer creates a cleartext server for compatible HTTP/2 clients. A normal browser deployment should use a secure server with the appropriate key, certificate chain, and TLS negotiation. ALPN negotiates the application protocol on the TLS connection. A valid certificate and hostname are still required; HTTP/2 is not a replacement for the trust rules explained in the HTTPS reference. A service can also terminate TLS and HTTP/2 at a reverse proxy and forward another protocol internally.
createSecureServer can be configured for supported HTTP/1 fallback through allowHTTP1, but that does not mean every low-level HTTP/2-only handler works unchanged for fallback requests. Use the compatibility API or explicit handling that matches the advertised protocols. Node's built-in fetch is not a substitute for the low-level HTTP/2 client in this example; use the documented http2.connect interface when teaching session behavior. Test the actual client and deployment protocol rather than claiming HTTP/2 support merely because a response header says so.
Bound multiplexing and close resources deliberately
Multiplexing reduces some connection overhead, but it does not provide unlimited capacity. Each active stream consumes resources, and the protocol has stream and flow-control settings. Limit application concurrency as well as individual body sizes. One slow consumer can interact with buffering and flow control, so consume or cancel bodies intentionally. HTTP/2 over TCP still has transport-level effects when packets are lost; multiplexing is not a universal latency guarantee for every workload.
A graceful session shutdown and a forced destruction serve different purposes. close allows the session to finish under its lifecycle rules, while destroy terminates it. GOAWAY communicates session shutdown behavior, but an operation interrupted during shutdown may have uncertain application completion. Retry only according to request semantics and any remote idempotency contract. A server listener also needs its sessions cleaned up before a complete shutdown can finish. The fixtures own one client session and close it after finite responses, then close the listener. Production shutdown should track all sessions and stop accepting new work deliberately.
Node.js documentation and version notes
Node.js HTTP/2 Sessions and Streams Examples
Each example shows the code, the output it produces and what each part does.
Sending a GET stream over a local h2c session
The complete .mjs fixture listens only on 127.0.0.1 and lets the operating system choose a port. It creates one h2c client session, sends a GET stream, prints the received status and body, and closes the session before the server. The server uses :status rather than an ordinary status header. Both the request stream and session have error handlers, and the awaited promise completes only after the response body ends. No TLS claim is made for this local cleartext connection. Use the HTTPS reference's certificate principles when adapting the architecture to a browser-facing secure deployment.
import http2 from 'node:http2';
import { once } from 'node:events';
const server = http2.createServer();
server.on('stream', stream => {
stream.on('error', () => {});
stream.respond({ ':status': 200, 'content-type': 'text/plain' });
stream.end('HTTP/2 report');
});
server.listen(0, '127.0.0.1');
await once(server, 'listening');
const client = http2.connect('http://127.0.0.1:' + server.address().port);
try {
await new Promise((resolve, reject) => {
client.on('error', reject);
const stream = client.request({ ':method': 'GET', ':path': '/report' });
let body = '';
stream.setEncoding('utf8');
stream.on('response', headers => console.log(headers[':status']));
stream.on('data', chunk => { body += chunk; });
stream.on('error', reject);
stream.on('end', () => { console.log(body); resolve(); });
stream.end();
});
} finally {
client.close();
await once(client, 'close');
await new Promise(resolve => server.close(resolve));
}200
HTTP/2 reportNode.js HTTP/2 Sessions and Streams in Real Projects
Run two concurrent streams on one session
Send two concurrent request streams through one session and collect their results in input order. The server responds with each requested path so both exchanges are distinguishable. Promise.all orders the returned results even if stream events arrive in another order. This practice proves shared-session request ownership, not a performance improvement from a two-message benchmark. The client helper handles each stream's own body and errors; the session also has its own error listener. Cleanup occurs only after both finite bodies have been consumed. A real load test would need realistic data, latency measurements, resource limits, and error scenarios.
import http2 from 'node:http2';
import { once } from 'node:events';
const server = http2.createServer();
server.on('stream', (stream, headers) => {
stream.on('error', () => {});
stream.respond({ ':status': 200 });
stream.end(headers[':path']);
});
server.listen(0, '127.0.0.1');
await once(server, 'listening');
const session = http2.connect('http://127.0.0.1:' + server.address().port);
session.on('error', () => {});
function request(path) {
return new Promise((resolve, reject) => {
const stream = session.request({ ':path': path });
let body = '';
stream.setEncoding('utf8');
stream.on('data', chunk => { body += chunk; });
stream.on('end', () => resolve(body));
stream.on('error', reject);
stream.end();
});
}
try { console.log(JSON.stringify(await Promise.all([request('/a'), request('/b')]))); }
finally {
session.close();
await once(session, 'close');
await new Promise(resolve => server.close(resolve));
}["/a","/b"]Node.js HTTP/2 Sessions and Streams FAQ
Can a browser use this cleartext server directly?
Do not assume so. The fixture uses a Node client explicitly supporting the h2c setup. Browser-facing HTTP/2 normally uses HTTPS with a valid certificate and ALPN negotiation. A reverse proxy can supply that secure protocol boundary. State and test the actual deployment prerequisites rather than presenting a loopback teaching connection as a complete production browser configuration.
Is one stream the same as one socket?
No. Several request-response streams can share one HTTP/2 session over a connection. A stream has its own headers and body, while session state governs the shared protocol relationship. Closing a response stream does not necessarily close the session. Keep per-request data distinct and close the session only according to its intended reuse and shutdown lifecycle.
Can I copy every HTTP/1 header into HTTP/2?
No. HTTP/2 has pseudo-headers and prohibits connection-specific headers such as Connection. Use the documented interface and lowercase header names. A compatibility layer can help when supporting several protocols, but it does not justify ignoring protocol constraints. Application media types and status meanings still apply, so protocol correctness and payload validation remain separate responsibilities.
Does multiplexing mean unlimited concurrent requests?
No. Streams consume memory and processing capacity, and the protocol includes limits and flow control. Apply application concurrency and body limits as well. Consume or cancel each response deliberately. A shared session reduces some overhead, but it can still fail or shut down, and uncertain completion needs a request-specific retry policy. Measure real workloads before promising a performance result.
Node.js HTTP/2 Sessions and Streams Exercises
Answer each one and check it instantly. If you get stuck, open the hint before the answer.
How many client sessions does the worked /a and /b example create?
Node.js HTTP/2 Sessions and Streams Quiz
2 questions to check what you learned.
Which field supplies the response status in the low-level server?
- The :status pseudo-header
- A Connection: status=200 header
- The session URL fragment
What remains necessary for browser-facing secure HTTP/2?
- Valid TLS configuration and appropriate protocol negotiation
- Only changing the content type to application/http2
- Disabling hostname verification so ALPN can succeed