# Connection Header

> Learn how the Connection header controls whether HTTP connections stay open (keep-alive) or close after each request. Optimize with persistent connections.

Source: https://howhttpworks.com/headers/connection
Last reviewed: 2026-10-05

> **TL;DR:** `Connection` controls the current HTTP/1.x connection. HTTP/1.1 keeps connections open by default, so you rarely need `Connection: keep-alive`; send `Connection: close` when you want the connection shut after this response. It also lists hop-by-hop headers that proxies must strip. HTTP/2 and HTTP/3 forbid it entirely.

`Connection` only covers the connection it travels on. A reverse proxy holds two separate connections, one to the client and one upstream. If a client sends `Connection: close`, that applies to the client-facing hop; the proxy makes its own choices for the upstream side.

## How Connection Works

This request asks the server to close the connection once it has responded (examples on this page are trimmed):

```http
GET / HTTP/1.1
Host: example.com
Connection: close
```

The connection closes after the response, not the moment the header arrives. Without `close`, either side may reuse the connection, but either side may also close it when it goes idle; nobody promises to keep it open forever. See [RFC 9112 §9.3](https://www.rfc-editor.org/rfc/rfc9112#section-9.3).

## Syntax

The value is a comma-separated list of case-insensitive tokens. Listing a header name marks that header as connection-specific, as with `Example-Hop` here:

```http
Connection: Example-Hop, close
Example-Hop: diagnostic-value
```

Before forwarding, an intermediary strips `Connection`, every header it names, and the other known connection-specific headers. [RFC 9110 §7.6.1](https://www.rfc-editor.org/rfc/rfc9110#section-7.6.1) defines the rules.

## HTTP Version Differences

| Protocol | Connection handling |
| --- | --- |
| HTTP/1.0 | Close by default; historical keep-alive extension is implementation-dependent. |
| HTTP/1.1 | Persistent by default; `close` ends reuse after the response. |
| HTTP/2 and HTTP/3 | `Connection` is forbidden, making the message malformed. |

HTTP/1.0 closes after each response unless both sides support the old `Connection: keep-alive` extension. HTTP/1.1 flipped the default to persistent. HTTP/2 and HTTP/3 go further: a connection-specific header isn't a harmless hint there, it makes the message malformed.

## Common Examples

In an HTTP/1.1 WebSocket handshake, the client sends this pair:

```http
Connection: Upgrade
Upgrade: websocket
```

That's only part of the handshake. The request also needs the headers described on [Sec-WebSocket-Key](https://howhttpworks.com/headers/sec-websocket-key).

## Real-World Scenarios

### Draining an HTTP server

`Connection: close` on a response ends reuse of that one connection; the server keeps listening. To shut down a Node HTTP/1.1 service cleanly, call `server.close()`, which stops accepting new connections and waits for active requests. This complete Express example also adds `Connection: close` to responses started while draining:

```javascript
import express from 'express'
import http from 'node:http'

const app = express()
let draining = false
app.use((req, res, next) => {
  if (draining) res.setHeader('Connection', 'close')
  next()
})
app.get('/health', (req, res) => {
  res.status(draining ? 503 : 200).json({ ready: !draining })
})
app.get('/', (req, res) => res.json({ status: 'ok' }))
const server = http.createServer(app)
server.listen(3000, '127.0.0.1')
process.once('SIGTERM', () => {
  draining = true
  server.close((error) => {
    if (error) console.error(error)
    process.exitCode = error ? 1 : 0
  })
})
```

Install Express with `pnpm add express`, save as `app.mjs`, and run `node app.mjs`. [Node documents `server.close()`](https://nodejs.org/api/http.html#serverclosecallback), including what it does with idle connections. Upgraded WebSocket connections need their own shutdown logic. Health checks are ordinary requests here; there's no need to close connections just because a health check came in.

### Long polling is an active request

A long-poll handler deliberately waits before responding. `Keep-Alive: timeout=60` is about idle time between requests, so it says nothing about a 60-second wait inside the handler; that's governed by the reverse proxy's upstream read timeout, which you check separately. Once the poll returns, even as a 204 with no body, HTTP/1.1 can reuse the connection. So there are two different timeouts in play: one that fires while the poll is pending, and one that reclaims the idle connection afterwards. To tell them apart, log when the request enters the handler, when headers are written, and when the socket closes.

## Keep-Alive Parameters

[Keep-Alive](https://howhttpworks.com/headers/keep-alive) can advertise idle-time and request-count hints, but sending the header changes nothing about how the server actually handles sockets. Configure the real limits on the server.

In Node, set the idle timeout and request count like this:

```javascript
server.keepAliveTimeout = 5000 // chosen idle timeout, milliseconds
server.maxRequestsPerSocket = 100 // chosen requests per socket
```

These go on an existing `http.Server` and only affect Node's side; a load balancer's connection pool has its own settings. `maxHeadersCount` is unrelated despite the name: it limits the number of header fields in a message.

Behind a load balancer, make the backend's idle timeout longer than the balancer's. AWS ALB defaults to 60 seconds; if the app closes idle connections first, the balancer can send a request down a socket the app just closed and return a 502. For Node behind an ALB, `server.keepAliveTimeout = 65000` is the usual setting. The [nginx 502 guide](https://howhttpworks.com/debug/nginx-502-bad-gateway) walks through this race.

## Getting Connection right

Keep application sessions and transport connections separate. A user logging out is no reason to close a socket; the two have different lifetimes. In browser JavaScript, `Connection` is a [forbidden request header](https://fetch.spec.whatwg.org/#forbidden-request-header), so the browser manages it for you.

## Connection Pooling

Node's HTTP client reuses connections through an `http.Agent`, passed to `http.get()` or `http.request()`. This runnable example reads each response fully before sending the next request, then destroys the pool:

```javascript
import http from 'node:http'

const agent = new http.Agent({ keepAlive: true })
function get(url) {
  return new Promise((resolve, reject) => {
    http.get(url, { agent }, (res) => {
      res.resume()
      res.on('end', resolve)
      res.on('error', reject)
    }).on('error', reject)
  })
}
try {
  await get('http://localhost:3000/')
  await get('http://localhost:3000/')
} finally {
  agent.destroy()
}
```

This is the [`node:http` Agent API](https://nodejs.org/api/http.html#class-httpagent). Other clients, including various `fetch()` implementations, have their own pooling options, so check before passing an `agent`.

For a busy client, cap active and idle sockets while keeping reuse on. Swap the Agent declaration above for something like this:

```javascript
const agent = new http.Agent({
  keepAlive: true,
  maxSockets: 20,
  maxFreeSockets: 5,
  maxTotalSockets: 40
})
```

`maxSockets` is per host, and `maxTotalSockets` covers every host the Agent talks to. Requests over the active limit wait in a queue. `maxFreeSockets` caps idle sockets in the pool; it doesn't limit concurrency. Pick numbers that fit your traffic; these are examples.

In the browser, the browser owns the pool. Parallel `fetch()` calls may share connections, open new ones, or ride multiplexed streams, so the number of promises tells you nothing about the number of connections.

## Debugging Connection Issues

Force HTTP/1.1 when you test these headers; over HTTPS, curl will otherwise negotiate HTTP/2 if the server offers it:

```bash
curl --http1.1 -v https://example.com/ https://example.com/ -o /dev/null
```

Put both URLs in one curl command, as above. Separate curl invocations each start with an empty pool, so they can't reuse anything. In the verbose output, look for curl's messages about reusing or closing the connection; a `Connection: keep-alive` header on its own tells you little.

To see reuse from the server side, attach listeners once per connection. Add this to the Node server above, before `listen()`:

```javascript
let nextSocketId = 0
const socketIds = new WeakMap()
server.on('connection', (socket) => {
  const id = ++nextSocketId
  socketIds.set(socket, id)
  console.log('opened', id)
  socket.on('close', () => console.log('closed', id))
})
server.on('request', (req) => {
  console.log('request', socketIds.get(req.socket), req.method, req.url)
})
```

When the same ID shows up on several requests, this Node process served them over one socket. Behind nginx, that's reuse between nginx and Node; browser-to-nginx reuse is a separate connection you'd check at nginx. Logging `remotePort` alone is weaker, since ports get reused across restarts. The socket comes from [Node's connection event](https://nodejs.org/api/http.html#event-connection).

If a connection closes after every request, check the response framing before blaming configuration. An HTTP/1.1 response with neither `Content-Length` nor chunked encoding may mark the end of its body by closing the connection, and then that socket can't carry another response. HEAD responses, 204 and 304 follow different body rules. [RFC 9112 §6.3](https://www.rfc-editor.org/rfc/rfc9112#section-6.3) sets out the order of precedence.

To see headers and bytes together on your own service:

```bash
curl --http1.1 --trace-ascii - http://127.0.0.1:3000/ -o /dev/null
```

Trace output includes credentials and body data, so run it against an unauthenticated local request. On the client side, make sure your code reads the whole response body. A connection only becomes available for the next request once the current body has been consumed.

## Testing

Send the same request with an explicit close and compare the verbose output:

```bash
curl --http1.1 -v -H 'Connection: close' https://example.com/ -o /dev/null
```

## HTTP/2 and the Connection Header

In HTTP/2 and HTTP/3, a message containing `Connection` is malformed ([RFC 9113 §8.2.2](https://www.rfc-editor.org/rfc/rfc9113#section-8.2.2), [RFC 9114 §4.2](https://www.rfc-editor.org/rfc/rfc9114#section-4.2)). Strip it when you translate HTTP/1.1 traffic to either protocol. If you don't, clients reject the response; in Chrome it shows up as [ERR_HTTP2_PROTOCOL_ERROR](https://howhttpworks.com/debug/err-http2-protocol-error).

## Related Headers

- [Keep-Alive](https://howhttpworks.com/headers/keep-alive)
- [Upgrade](https://howhttpworks.com/headers/upgrade)
- [Transfer-Encoding](https://howhttpworks.com/headers/transfer-encoding) — chunked framing can permit HTTP/1.1 reuse without Content-Length.
- [Content-Length](https://howhttpworks.com/headers/content-length)
