Skip to content

Instantly share code, notes, and snippets.

@benjie
Last active June 19, 2025 12:54
Show Gist options
  • Save benjie/54572a6781963c4231bc611d5e916c57 to your computer and use it in GitHub Desktop.
Save benjie/54572a6781963c4231bc611d5e916c57 to your computer and use it in GitHub Desktop.
HTTP/2 has ~100 connection limit in browsers

The HTTP/2+ protocol itself allows for virtually unlimited connections over the multiplex, but browsers generally limit to ~100. Since this isn't well documented, I've written a demonstration that shows that connections over 100 don't connect (tested in both Firefox and Chrome).

image

To run:

  1. Use Node 22+
  2. Create certificates: openssl req -x509 -newkey rsa:2048 -nodes -keyout server.key -out server.crt -days 365
  3. Write the code to server.mts
  4. Run it with node --experimental-strip-types server.mts
  5. Open https://localhost:8888 in your browser and accept the self-signed certificate

You should observe randomly flashing divs 1-100, with divs 101-105 sitting there idle.

CONNECTION LIMITS RESULT IN A HANG

CONNECTION LIMITS ARE PER-SITE, NOT PER-TAB

Since subscriptions should generally be small (to individual items) you often end up with a lot of them. Subscriptions are also long-lived. One connection per subscription is therefore not viable for most use cases. Ensure that you use a transport that allows multiplexing all subscriptions over the same "virtual" connection:

  • websockets
  • SSE in "single connection mode"
import { readFileSync } from "node:fs";
import { createSecureServer } from "node:http2";
const MAX_DELAY = 5_000;
const home = /* HTML */ `
<!doctype html>
<html>
<head>
<title>SSE connection count test</title>
<style>
body {
font-family: sans-serif;
}
.connection {
width: 14rem;
height: 2rem;
box-sizing: border-box;
border: 1px solid #ccc;
padding: 0.25rem;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
color: black;
transition: background-color 4s ease;
}
.connection {
margin: 4px 0;
}
.connecting {
color: gray;
}
.connected {
color: green;
}
.message {
background-color: #00ff0099;
transition: background-color 0s ease;
}
.disconnected,
.error {
color: red;
}
#connections {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
</style>
</head>
<body>
<h1>SSE Connections</h1>
<div id="connections"></div>
<script>
const container = document.getElementById("connections");
const COUNT = 105;
for (let i = 1; i <= COUNT; i++) {
const id = i;
const div = document.createElement("div");
div.className = "connection connecting";
div.textContent = id + ": connecting...";
container.appendChild(div);
const es = new EventSource("/sse/" + id);
es.onmessage = (event) => {
div.className = "connection connected message";
div.textContent = id + ": message: " + event.data;
setTimeout(() => {
div.className = "connection connected";
}, 10);
};
es.onerror = () => {
if (es.readyState === EventSource.CLOSED) {
div.className = "connection disconnected";
div.textContent = id + ": disconnected";
} else {
div.className = "connection error";
div.textContent = id + ": error";
}
};
}
</script>
</body>
</html>
`;
const notFound = `
<!doctype html>
<html>
<head>
<title>Not found</title>
</head>
<body>
<h1>Not found</h1>
<a href="/">Go home</a>
</body>
</html>
`;
const server = createSecureServer(
{
// Create with:
// openssl req -x509 -newkey rsa:2048 -nodes -keyout server.key -out server.crt -days 365
key: readFileSync("server.key"),
cert: readFileSync("server.crt"),
},
(req, res) => {
if (req.url === "/") {
res.writeHead(200, { "content-type": "text/html" });
res.end(home);
} else if (req.url?.startsWith("/sse/")) {
const started = Date.now();
const id = req.url.split("/").pop();
res.writeHead(200, {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
"Access-Control-Allow-Origin": "*",
});
let timer: any;
function next() {
const uptime = ((Date.now() - started) / 1000).toFixed(1);
res.write(`data: ${id}: ${uptime}\n\n`);
timer = setTimeout(next, Math.random() * MAX_DELAY);
}
next();
req.on("close", () => {
clearTimeout(timer);
});
} else {
res.writeHead(404, { "content-type": "text/html" });
res.end(notFound);
}
},
);
server.listen(8888);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment