Skip to the solution
HardBackend7 min read

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

487 views

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.

  • nodejs
  • http2
  • networking
  • performance

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

7 min read Β· 7 sections

Read the code

JavaScriptReal 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();
  });
});

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 135 of 152 in Node.js

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All Node.js questions
Up nextHard

What are the Web Streams API (ReadableStream/WritableStream) in Node.js and how do they differ from node:stream?

Continue