Opening a connection is not free. TCP costs one round trip, TLS costs another, and only then does the request go out. On a 60ms link that is 120ms spent before your server has been asked anything - per connection.

Keep-alive means: do that once

With keep-alive the browser reuses the same connection for the next twenty files. It is on by default in every modern server, which is why this article is mostly about not breaking it.

# nginx defaults, and they are sensible
keepalive_timeout 65;
keepalive_requests 1000;

Check that you have it

curl -sSI https://yourdomain.com/ | grep -i -E 'connection|http/'

HTTP/2 or HTTP/3 in the status line means connection reuse is already happening and is not something you need to configure - see HTTP/2 and HTTP/3. Connection: close on an HTTP/1.1 response is the thing to investigate.

The three things that break it

  • An old load balancer or proxy in front that closes connections itself.
  • A misconfigured upstream block - Nginx talking to your application without a keepalive pool.
  • Serving assets from many different hostnames, so no connection is reused for the next file.
upstream app {
    server 127.0.0.1:9000;
    keepalive 32;          # the pool Nginx keeps open to your app
}

location / {
    proxy_http_version 1.1;
    proxy_set_header Connection "";   # required, or the pool is not used
}
Serving fonts, images and scripts from your own domain is now faster than a third-party host: same connection, no extra DNS, no extra TLS. Splitting assets across hostnames was advice for HTTP/1.1 and is a cost today.