# Content-Length

> Learn how Content-Length specifies the body size in bytes. Essential for progress indicators, connection management, and chunked transfer decisions.

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

> **TL;DR:** `Content-Length` is the size of the message body in bytes, written as a plain decimal number. Count bytes, not characters, and count them after compression: for a gzipped body it's the gzipped size. HTTP/1.1 chunked responses leave it out, and HTTP/2 and HTTP/3 don't need it.

## What is Content-Length?

The header gives the body size in bytes. In HTTP/1.1 it's how the receiver knows where one message ends and the next begins. In HTTP/2 and HTTP/3 the framing handles that, but the header still tells the receiver how much to expect. [RFC 9110 §8.6](https://www.rfc-editor.org/rfc/rfc9110#section-8.6) defines it.

The value is digits only. `Content-Length: 5` is valid; `Content-Length: 5 bytes` isn't. No units, signs, or thousands separators.

## How It Works

In this example response (trimmed, like the others on this page), `Hello` is exactly five ASCII bytes with no trailing newline:

```http
HTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: 5

Hello
```

The count covers the body only. Headers and chunk framing don't count.

## Why It Matters

With a known length, an HTTP/1.1 client knows when the response is done and can reuse the connection for the next request. Chunked encoding allows reuse too, so leaving out `Content-Length` doesn't by itself force the connection to close.

For download progress bars, note that browser APIs can hand you decompressed bytes while the header counts compressed ones. Dividing one by the other doesn't give you progress.

## Calculating Content-Length

`Café` has four JavaScript string code units and five UTF-8 bytes:

```javascript
const body = Buffer.from('Café', 'utf8')
console.log(body.length) // 5
```

If you gzip the body, measure the compressed buffer, not the original string. [Node's HTTP documentation](https://nodejs.org/api/http.html#responsewriteheadstatuscode-statusmessage-headers) also calls this out: use byte length, not character length.

## When Content-Length is Required

No rule says every request with a body needs it. HTTP/1.1 requests can use chunked encoding, though a server is allowed to refuse those with `411 Length Required`. If you know the length up front, send it instead of chunking; see [RFC 9112 §6.3](https://www.rfc-editor.org/rfc/rfc9112#section-6.3).

An HTTP/1.1 request with neither `Transfer-Encoding` nor `Content-Length` has an empty body. Unlike a response, a request can't mark the end of its body by closing the connection.

## When Content-Length is Not Used

Leave it off any message that uses `Transfer-Encoding`. It's also not allowed on `1xx` and `204` responses, or on a successful (2xx) response to `CONNECT`.

Two cases look odd but are fine. A `HEAD` response can carry the length a GET would have sent, even though it has no body. A `304` can carry the length of the full `200` response. In both, the number describes the resource; no body follows.

## Content-Length vs Transfer-Encoding

With chunked encoding, each chunk carries its own size and a zero-length chunk marks the end. A response with neither header just ends when the server closes the connection. [HTTP/2](https://www.rfc-editor.org/rfc/rfc9113#section-8.1.1) and [HTTP/3](https://www.rfc-editor.org/rfc/rfc9114#section-4.1.2) use their own frames, but if you do send `Content-Length`, it has to match the bytes actually sent (except on responses that never have a body, like HEAD and 304).

## Common Issues

A wrong length breaks more than progress bars. A body shorter than declared is incomplete. A declared length that's too small leaves extra bytes that get parsed as another message. Treat any mismatch as a protocol error.

To check a real server, look at an actual GET, not just a HEAD:

```bash
curl --http1.1 -sS -D /tmp/length.headers -o /tmp/length.body \
  -H 'Accept-Encoding: identity' https://example.com/
cat /tmp/length.headers
wc -c /tmp/length.body
```

Check `Content-Encoding` first. If the body came back compressed, compare the length with the compressed size.

## Getting Content-Length right

For streamed bodies, let your HTTP library pick the framing. If middleware changes the body after the length was set, recalculate the length or drop it before the headers go out. A proxy that knows a length is wrong must not pass it along.

For JSON, serialize once and count the UTF-8 bytes of the result:

```javascript
const body = JSON.stringify({ message: 'Café' })
const length = Buffer.byteLength(body, 'utf8')
```

Gzip that body afterwards and `length` is wrong. Either count the compressed buffer, or let your compression middleware drop the old header and pick the framing.

## Server Examples

A complete Node server that counts the bytes it actually sends:

```javascript
const http = require('node:http')
http.createServer((req, res) => {
  const body = Buffer.from('Café', 'utf8')
  res.writeHead(200, {
    'Content-Type': 'text/plain; charset=utf-8',
    'Content-Length': body.length
  })
  res.end(body)
}).listen(3000)
```

To serve a file in Go, use [`http.ServeContent`](https://pkg.go.dev/net/http#ServeContent) instead of setting the length yourself and copying bytes:

```go
func report(w http.ResponseWriter, r *http.Request) {
    f, err := os.Open("/srv/reports/report.pdf")
    if err != nil {
        http.Error(w, "Report unavailable", http.StatusNotFound)
        return
    }
    defer f.Close()
    info, err := f.Stat()
    if err != nil {
        http.Error(w, "Cannot stat report", http.StatusInternalServerError)
        return
    }
    http.ServeContent(w, r, "report.pdf", info.ModTime(), f)
}
```

Drop this handler into a Go server that imports `net/http` and `os`. `ServeContent` seeks to find the size and handles range and conditional requests. A range response's length is the size of the returned slice, so hard-coding the full file size would be wrong.

## Related Headers

- [Transfer-Encoding](https://howhttpworks.com/headers/transfer-encoding)
- [Content-Encoding](https://howhttpworks.com/headers/content-encoding)
- [Content-Type](https://howhttpworks.com/headers/content-type)
