How to check if keep-alive is enabled
Ask for two URLs on one curl command line and print %{num_connects} for each: curl -s -o /dev/null -o /dev/null -w '%{url_effective} conn=%{num_connects}\n' https://host/a https://host/b. A 1 then a 0 means the second request rode the first connection. A 1 twice means the handshake was paid again.
Why check this
Run this on staging sign-off for anything that sits behind a new proxy, and after any load-balancer change. A server that closes the connection after every response forces a fresh TCP and TLS handshake per request. On this machine that cost 0.43 s of TLS setup against httpbin.org, repeated for every asset on the page. The site still works, every test passes, and page load doubles for users far from the origin.
Prerequisites
- curl 7.x or later. No HTTP/2 support is needed; reuse is measurable on HTTP/1.1 as well.
- Two paths on the same host that both return a response, so the second request has somewhere to go.
- The curl write-out reference for the meaning of each variable used below.
Steps
- Step 1.
Read what the server states on HTTP/1.1, where the header exists at all.
curl -sI --http1.1 https://httpbin.org/get | grep -i -E '^HTTP/|^connection'HTTP/1.1 200 OK Connection: keep-aliveThis is the server's declared intent. It is not evidence that a second request reused the socket.
- Step 2.
Measure the reuse. Two URLs, one command, one connection counter each.
curl -s -o /dev/null -o /dev/null -w '%{url_effective} conn=%{num_connects} tls=%{time_appconnect} total=%{time_total}\n' https://httpbin.org/get https://httpbin.org/headershttps://httpbin.org/get conn=1 tls=0.426159 total=0.617525 https://httpbin.org/headers conn=0 tls=0.000000 total=0.138289conn=0on the second line is the result.tls=0.000000confirms the same thing from the other side: no handshake happened, and the request finished in 0.138 s instead of 0.618 s. - Step 3.
Confirm it in words rather than numbers, for a bug report.
curl -sv -o /dev/null -o /dev/null https://httpbin.org/get https://httpbin.org/headers 2>&1 | grep -i -E 'Established connection|Reusing existing|left intact'* Established connection to httpbin.org (18.235.200.183 port 443) from 192.168.0.109 port 54972 * Connection #0 to host httpbin.org:443 left intact * Reusing existing https: connection with host httpbin.org * Connection #0 to host httpbin.org:443 left intactOne connection established, then reused. When a server closes after each response, the second line is another
Established connectioninstead ofReusing existing. - Step 4.
Force the opposite case so you know what a closing server looks like.
curl -sI --http1.1 -H 'Connection: close' https://httpbin.org/get | grep -i -E '^HTTP/|^connection'HTTP/1.1 200 OK Connection: closeThe server echoes the decision. If your own service answers
closewithout being asked, that is the defect. - Step 5.
Check that the flag named after keep-alive is not the one under test.
curl -s --no-keepalive -o /dev/null -o /dev/null -w '%{url_effective} conn=%{num_connects}\n' https://httpbin.org/get https://httpbin.org/headershttps://httpbin.org/get conn=1 https://httpbin.org/headers conn=0Reuse still happened with
--no-keepaliveset. That option controls TCP keepalive probes on an idle socket, not HTTP connection reuse.
How to read the result
| What you see | What it means | What to do |
| --- | --- | --- |
| conn=1 then conn=0 | The connection was reused | Nothing. This is the shape you want on every host. |
| conn=1 on every line, same host | Each request opened a new connection | Read step 4's header on your service. A proxy or an idle timeout of zero is the usual cause. |
| Connection: keep-alive but conn=1 twice | The header is set and something closes anyway | Look between curl and the app: a load balancer can close what the origin keeps open. |
| time_appconnect above zero on a later request | TLS was negotiated again | Same as above. This is the cost being paid, in seconds. |
| conn=1 for two different hostnames | Expected | Reuse is per host and port. Two names never share a connection, even on one IP. |
Common mistakes
Thresholds
time_appconnect on the first request, which is TCP plus TLS setup. That is what a non-reusing server charges for every single request, and it is the number to quote when asking for the configuration to change.
What to check next
- How to check if a website uses HTTP/2: one connection carrying every request only pays off when it stays open.
- How to check TTFB: the first request carries the handshake, so compare it with a warm one.
- How to check API response time: splits the same total into DNS, TCP, TLS and server time.
- How to check if CDN cache is hit or miss: the other reason a second request is faster than the first.
FAQ
How do I check if curl reuses a connection?
Print %{num_connects} per URL, as in step 2, or read the verbose line Reusing existing https: connection. Both report the same fact. The counter is easier to assert on in a script.
Why does keep-alive matter if HTTP/2 is enabled?
HTTP/2 multiplexes over one connection, so closing that connection costs more than closing an HTTP/1.1 one. A proxy with a one-second idle timeout cancels most of the benefit of moving to HTTP/2 at all.
Is Connection: keep-alive needed on HTTP/1.1?
No. Persistent connections are the default in HTTP/1.1 and the header is informational. What matters is whether the socket is still open for the next request, which step 2 measures directly.
How many requests should I test with?
Two is enough to prove reuse. Use more when checking an idle timeout, spacing them with sleep and watching for the first request that reports conn=1 again.
Verified
Verified by Maks Vernycurl 8.21.0
Each output block is what the command above it printed on that date, on the host named in the step. Figures read from a live site move between runs. Compare the shape of the answer rather than the digits, and see the methodology for how a page is re-verified.
Related on this site
basic5 minpublished updated Maks Verny