Konrad Kowalski (rootsher)Principal Platform & Reliability Architect111011101101011101001111100100000101011101011011

Connections: Why One fetch() Does Not Mean One Connection

date
category
Frontend
also in
Networking
reading
2 min / 339 words

In code:

js
await fetch("/api/a");
await fetch("/api/b");

we see two requests.

That does not mean:

text
two requests
=
two TCP connections

An HTTP request and a transport connection have different lifecycles.

Connection setup costs

For classic HTTPS over TCP the path includes at least:

text
DNS
  |
  v
TCP connection
  |
  v
TLS
  |
  v
HTTP

If every fetch() built that relationship from scratch, setup latency would dominate for small requests.

That is why the browser reuses connections.

HTTP/1.1 persistent connections

In modern HTTP/1.1 a connection is persistent by default, unless the parties agree to close it.

So:

text
TCP connection
├── request A -> response A
├── request B -> response B
└── request C -> response C

That removes the cost of establishing TCP and TLS for every request.

But HTTP/1.1 has a limitation: concurrency on a single connection is problematic.

Which led browsers to keep several connections to an origin.

fetch() does not control the socket pool

The frontend does not write:

js
fetch(url, {
  tcpConnection: 3
});

The browser manages the connection pool.

It decides:

  • whether to reuse an existing connection,
  • whether to establish a new one,
  • which protocol was negotiated,
  • whether a request may share a connection with other requests.

The frontend influences that indirectly, through the architecture of its resources and origins.

The origin matters

Requests to:

text
https://app.example.com
https://api.example.com
https://cdn.example.com

are not automatically one logical connection pool just because they belong to the same company.

Historically the frontend tried to increase parallelism by spreading assets across several hostnames:

text
img1.example.com
img2.example.com
img3.example.com

That was called domain sharding.

It was an answer to the limitations of HTTP/1.1.

Under HTTP/2 it often becomes counterproductive, because it splits traffic that could share one multiplexed connection.

Keep-alive in fetch() is a different topic

Fetch has an option:

js
fetch("/analytics", {
  method: "POST",
  body: data,
  keepalive: true
});

The name can mislead.

It is not a switch for HTTP persistent connections.

Fetch keepalive is about allowing a specific request to continue after the page starts unloading, for example for small telemetry.

Do not mix up:

text
Fetch keepalive option

with:

text
HTTP connection persistence

The frontend problem with HTTP/1.1

So we have persistent connections, but a single connection does not yet give us convenient concurrency for many independent responses.

And a typical page needs:

text
HTML
CSS
JS
fonts
images
API calls

The limitations of that model are exactly what produced the characteristic HTTP/1.1 network waterfall.

And for years they shaped the way we built frontend bundles.