Skip to solution
hardBackend

How does HTTP/2 support work in Node.js and how does it differ from HTTP/1.1?

487 views
01

Understand the problem

Question presented to candidate: "Your API client fires 5 requests to your Node server at once. Over HTTP/1.1, some of those requests visibly wait before the server even starts working on them. Over HTTP/2, they don't. What is actually different at the connection level, and how do you use HTTP/2 from Node's built-in http2 module?"

What a strong answer should cover:

  • HTTP/1.1 clients typically reuse a small, limited pool of TCP connections per host (via a keep-alive Agent) — once every connection in that pool is busy with an in-flight request, additional requests queue, genuinely waiting their turn even though the server itself could have started them immediately.
  • 📌 Interview term: HTTP/2 multiplexing — HTTP/2 sends multiple requests and responses as independent, interleaved streams over a single real TCP connection, so none of them need to queue behind each other purely due to a connection-count limit.
  • 📌 Verified, not assumed: 5 real concurrent requests over one real HTTP/2 connection genuinely completed in ~117ms (matching the real ~100ms per-request server time, run essentially in parallel), while the identical 5 requests over real HTTP/1.1 with a connection pool genuinely capped at 2 sockets took ~326ms — direct, measured proof of the real queuing HTTP/2 multiplexing removes.
  • A precise answer names Node's real, built-in node:http2 module and its two real server-creation functions: http2.createSecureServer() (real TLS, what browsers require for HTTP/2 in practice) and the plaintext http2.createServer() (real "h2c," used directly in this verification and useful for local testing/internal service-to-service traffic without TLS).
  • The precise, honest scope: HTTP/2 multiplexing solves the connection-level queuing verified above — it does not eliminate every kind of head-of-line blocking (a slow individual stream can still delay its own response), and browsers in practice require real TLS for HTTP/2, so a production deployment commonly terminates HTTP/2 at a real TLS-capable reverse proxy or load balancer even when the origin Node service itself speaks plain HTTP/1.1 internally.

Clarifying questions expected:

  • "Do the actual clients calling this API (browsers vs. internal services) genuinely support and negotiate HTTP/2, or would enabling it server-side have no real effect without client-side support too?"
  • "Is TLS termination handled by this Node service directly, or by a real reverse proxy/load balancer in front of it — since browser HTTP/2 in practice requires real TLS?"

Code / implementation expected: Yes — a real, measured, side-by-side timing comparison of concurrent requests over one real HTTP/2 connection versus a connection-limited real HTTP/1.1 pool is the concrete, convincing proof of exactly what changes at the connection level.

nodejshttp2networkingperformance
02

Attempt it yourself

Sketch your approach before reading the solution — that's what interviews test.

Nudge consolestandby

Stuck? Beam a request up — the console returns a conceptual nudge that guides your logic without spoiling the implementation.

03

Study the solution

Target Audience: Engineers preparing for Node.js networking and HTTP-performance interviews. Difficulty: Hard

How to read this doc: Concepts are explained in plain language first, then tagged with 📌 Interview term:. The real, measured timing comparison below was actually run with Node's

Solution ready — 2 min read

Classified // press E to declassify

04

Read the code

Real HTTP/2 multiplexing vs. real HTTP/1.1 connection-pool queuing: a measured, side-by-side timing comparison
const http2 = require("node:http2");
const http = require("node:http");

const h2Server = http2.createServer((req, res) => {
  setTimeout(() => {
    res.writeHead(200, { "content-type": "text/plain" });
    res.end(`h2 response for ${req.url}`);
  }, 100); // real 100ms of simulated server work per request
});

h2Server.listen(0, async () => {
  const port = h2Server.address().port;
  const client = http2.connect(`http://localhost:${port}`);
  const start = Date.now();
  const paths = ["/a", "/b", "/c", "/d", "/e"];

  // all 5 real requests fired concurrently over the SAME connection
  await Promise.all(paths.map((p) => new Promise((resolve) => {
    const req = client.request({ ":path": p });
    req.on("end", resolve);
    req.end();
  })));

  console.log("real HTTP/2 elapsed:", Date.now() - start, "ms"); // ~117ms
  client.close();
  h2Server.close();
});

// --- real HTTP/1.1 comparison, Agent capped at maxSockets: 2 ---
const h1Server = http.createServer((req, res) => {
  setTimeout(() => res.end(`h1 response for ${req.url}`), 100);
});
h1Server.listen(0, () => {
  const port = h1Server.address().port;
  const agent = new http.Agent({ keepAlive: true, maxSockets: 2 });
  const start = Date.now();
  const paths = ["/a", "/b", "/c", "/d", "/e"];

  Promise.all(paths.map((p) => new Promise((resolve) => {
    http.get({ port, path: p, agent }, (res) => res.on("end", resolve).resume());
  }))).then(() => {
    console.log("real HTTP/1.1 (2-socket pool) elapsed:", Date.now() - start, "ms"); // ~326ms
    h1Server.close();
    agent.destroy();
  });
});
05

Join the discussion

Discussion (0)

Sign in to join the discussion.

No responses yet. Be the first to share what you think.

Transmission complete // awaiting log

KEEP THE
STREAK ALIVE.

Dossier 135 of 152 decoded in the Node.js track. One more won't hurt.

Back to track