Connections: Why One fetch() Does Not Mean One Connection
- date
- category
- Frontend
- also in
- Networking
- reading
- 2 min / 339 words
In code:
await fetch("/api/a");
await fetch("/api/b");
we see two requests.
That does not mean:
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:
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:
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:
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:
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:
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:
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:
Fetch keepalive option
with:
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:
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.