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

Steps

  1. 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-alive

    This is the server's declared intent. It is not evidence that a second request reused the socket.

  2. 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/headers
    
    https://httpbin.org/get conn=1 tls=0.426159 total=0.617525
    https://httpbin.org/headers conn=0 tls=0.000000 total=0.138289

    conn=0 on the second line is the result. tls=0.000000 confirms the same thing from the other side: no handshake happened, and the request finished in 0.138 s instead of 0.618 s.

  3. 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 intact

    One connection established, then reused. When a server closes after each response, the second line is another Established connection instead of Reusing existing.

  4. 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: close

    The server echoes the decision. If your own service answers close without being asked, that is the defect.

  5. 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/headers
    
    https://httpbin.org/get conn=1
    https://httpbin.org/headers conn=0

    Reuse still happened with --no-keepalive set. 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

Sign: Two separate curl commands both report conn=1, so keep-alive is declared broken.Cause: Each curl process starts with an empty connection pool and exits afterwards. Reuse can only be observed inside one process, which is why step 2 puts both URLs on one command line.
Sign: --keepalive-time or --no-keepalive is used to test HTTP keep-alive.Cause: Those options set TCP keepalive probes on an idle socket, a transport-level heartbeat with no relation to HTTP connection reuse. Verified in step 5: with --no-keepalive the second request still reported conn=0.
Sign: The Connection header is missing and the tester concludes keep-alive is off.Cause: HTTP/2 and HTTP/3 have no Connection header at all; the field is forbidden there and reuse is the default. Add --http1.1 when you want to read the header, and use num_connects when you want the fact.

Thresholds

0.426 s

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.

Source: Measured here on 2026-09-11 with curl 8.21.0 against httpbin.org

What to check next

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.

basic5 minpublished updated Maks Verny