If Caddy answers 431 Request Header Fields Too Large after an upgrade, the cause is the 16 KiB request header limit that arrived with 2.11.6; it used to be 1 MB. Raise it with max_header_size inside servers in the global options. The same release silently drops headers whose name contains a dot, cuts uploads and responses that stall for 60 s, and stops inheriting client_auth from a wildcard site. I reproduced all of it on 18 October 2026 in Docker with caddy:2.11.4 and caddy:2.11.7 and the same Caddyfile, and below is the option that fixes each change. This guide is also available in Spanish.

Key takeaways

  • Caddy 2.11.6 (1 October 2026) caps request headers at 16 KiB. In my HTTP/1.1 test, 2.11.7 accepted a 20,397-byte cookie and answered 431 with one more byte; over HTTP/2 the cut came at 16,377 bytes.
  • Headers with a . in the name, such as X-Api.Key, no longer reach the backend and nothing shows up in the log. You allow them back one by one with expected_dot_headers.
  • An upload that stalls for 75 s ended in 504 Gateway Timeout after 60 s. read_body_idle under timeouts, or the per-route timeouts directive, fixes it.
  • A subdomain with its own site block no longer inherits mutual TLS (mTLS) from *.domain. If you relied on that, the site is now open without a client certificate.
  • There is no caddy:2.11.5 image: the v2.11.5 tag was created on 30 September with no binaries and no notes. In Docker the jump is from 2.11.4 to 2.11.6 or 2.11.7.
  • Use 2.11.7 (3 October): it fixes a 2.11.6 bug that cut SSE streams opened with POST after 60 s. As of 18 October 2026 there is no later release.

What does 431 Request Header Fields Too Large mean in Caddy?

Status 431 means the server refuses the request because its headers, together or a single one, are larger than it accepts. Caddy is written in Go, and up to 2.11.4 it used the default limit of Go’s HTTP server, 1 MB. The 2.11.6 release notes put it this way: "Request headers are limited to 16 KiB by default. Before, we used Go’s 1 MB default." This is what the browser, or curl -i, shows:

HTTP/1.1 431 Request Header Fields Too Large
Content-Type: text/plain; charset=utf-8
Connection: close

431 Request Header Fields Too Large

Chromium shows that same plain-text line, without Caddy’s error page, because Go answers before the request reaches any handler. That is also why nothing appears in Caddy’s access log. The usual culprits are cookies piled up by two or three apps under one domain and large tokens in Authorization, such as a JWT from an identity provider that lists hundreds of groups.

How I reproduced the Caddy 2.11.6 changes

I ran three Caddy containers from the official Docker Hub images: 2.11.4, 2.11.6 and 2.11.7. All three sit in front of the same backend, a Python server that returns the headers it received and the body size. Everything ran on Docker 29.5.2 on arm64 Linux, with curl 8.14.1 and PHP-FPM 8.4.26 for the PHP test. The shared Caddyfile has an HTTP proxy and two TLS sites with internal certificates:

http://:80 {
    reverse_proxy echo:8000
}

*.lab.test {
    tls internal {
        client_auth {
            mode require_and_verify
            trust_pool file /lab/ca.pem
        }
    }
    respond "wildcard site {host}, mTLS ok"
}

admin.lab.test {
    tls internal
    respond "admin panel {host}"
}

I wanted to test 2.11.5, but docker pull caddy:2.11.5 returns not found. The v2.11.5 tag has existed in the repository since 30 September 2026, with no notes and only the source code, and 2.11.6 came out the next day. If you run Caddy in Docker, your last version before the change is 2.11.4.

How to fix the 431: raise max_header_size

max_header_size is a servers option in the global block and takes sizes such as 64KiB. With this block at the top of the Caddyfile, 2.11.7 accepted 12 cookies of 3,500 bytes (about 42 KB) again, over HTTP/1.1 and HTTP/2:

{
    servers {
        max_header_size 64KiB
    }
}

Before you pick a value, note that the real limit depends on the protocol. Over HTTP/1.1, Go’s server adds 4,096 bytes of margin when it reads headers ("bufio slop" in its source code). So with the default, 2.11.7 accepted a 20,397-byte cookie and returned 431 at 20,398.

HTTP/2 has no margin: the last accepted value was 16,377 bytes, and from 16,697 bytes Go stops answering 431 and closes the connection instead. With curl it looks like this:

curl: (16) Error in the HTTP2 framing layer

If your users see an HTTP/2 protocol error in the browser instead of the 431, the cause and the fix are the same. Measure what you need before raising the cap blindly: a higher limit also lets larger requests in from hostile clients.

Headers with a dot that disappear: expected_dot_headers

Since 2.11.6, Caddy drops request headers whose name contains a dot, the same way 2.11.4 started dropping the ones with an _ in the name. I sent X-Api.Key: s3cr3t to both versions: 2.11.4 delivered it to the backend and 2.11.7 did not, with no log line. The reason is PHP. I checked with PHP-FPM behind php_fastcgi that PHP turns the dot into an _, and on 2.11.4 a request with both headers overwrites the legitimate one:

curl -s -H "X-Api-Key: real-key" -H "X-Api.Key: injected" \
  http://127.0.0.1:30803/

On 2.11.4, $_SERVER['HTTP_X_API_KEY'] is injected; on 2.11.7 it is real-key. If your app does expect a header with a dot, allow it by name:

{
    servers {
        expected_dot_headers X-Api.Key
    }
}

The option takes a list of names and a trailing * as a prefix, and it is case-insensitive. Watch for one effect I confirmed: when a dotted header is allowed, Caddy drops its hyphenated variant. With X-Api.Key allowed and both headers in the request, the backend only received X-Api.key: injected. Allow only the names your app uses with a dot.

Uploads cut at 60 s: read_body_idle and write_idle

Version 2.11.6 added two default idle deadlines of 1 minute each. read_body_idle aborts the body read when the client stops sending data, and write_idle aborts the response when a write makes no progress. A pause between writes does not count, so an SSE (Server-Sent Events) stream that sends one event every 10 s is not affected.

To test it, my script sends half of a 20-byte body, waits 75 s and sends the rest. On 2.11.4 the request ended in 200; on 2.11.7 it ended in 504 Gateway Timeout after 60 s, with this log line:

"msg":"readfrom tcp 172.23.0.5:54346->172.23.0.3:8000:
  read tcp 172.23.0.5:80->172.23.0.1:46344: i/o timeout"
"duration":60.000733277,"status":504

This hits clients that upload from slow networks and pause, resumable uploads, and tools that open the request before they have the data. There are two fixes and I tested both. The first raises the deadline for the whole server:

{
    servers {
        timeouts {
            read_body_idle 5m
        }
    }
}

The second tunes it per route with the timeouts directive, which is experimental. The server deadlines are applied afterwards and win, so the documentation says to disable the global one with a negative value. With this configuration, the upload stalled for 75 s on /upload ended in 200 and the one on /api/import in 504:

{
    servers {
        timeouts {
            read_body_idle -1s
        }
    }
}

http://:80 {
    timeouts /upload* {
        read_timeout 5m
    }
    timeouts {
        read_timeout 1m
    }
    reverse_proxy echo:8000
}

I prefer the second: it keeps the slowloris protection on everything else. For responses, write_idle works the same way as read_body_idle.

client_auth is no longer inherited from the wildcard site

Since 2.11.6 (PR 7920), a wildcard site’s client_auth no longer applies to a more specific hostname that has its own site block. With the Caddyfile above, admin.lab.test required a client certificate on 2.11.4 because *.lab.test did; curl without a certificate failed with tlsv13 alert certificate required. On 2.11.7 the same request without a certificate returned admin panel admin.lab.test.

The change fixes a real bug: a site that never configured mTLS got it by accident. But if you counted on that inheritance to protect a panel, the upgrade leaves it open. Repeat client_auth in every block with a reusable snippet:

(lab_mtls) {
    client_auth {
        mode require_and_verify
        trust_pool file /lab/ca.pem
    }
}

admin.lab.test {
    tls internal {
        import lab_mtls
    }
    respond "admin panel {host}"
}

With the snippet imported in *.lab.test as well, 2.11.7 again refused admin.lab.test without a certificate and served it with one.

Other Caddy 2.11.6 breaking changes I did not test

The 2.11.6 notes list more changes that can break a configuration and that I did not reproduce. Review them before you upgrade:

  • Go 1.26: the minimum version to build Caddy and its plugins, which matters if you build with xcaddy or the builder image
  • Stricter validation: duplicate named_routes, a repeated uri in forward_auth, invalid weighted_round_robin weights and ambiguous map entries are now rejected
  • Admin API /load: an invalid config returns 400 with a valid JSON body, where it used to return 200
  • method matcher: values are normalized to uppercase, so method get matches GET requests

What changed in Caddy 2.11.7, and is there a later release?

Version 2.11.7 came out on 3 October 2026 to fix regressions from 2.11.6, and its notes are blunt: "If you’re on 2.11.6, we recommend upgrading." It does not touch the header limit, dotted headers or client_auth. What it fixes are three bugs in the new deadlines:

  • a nil pointer crash when proxying over HTTP/2
  • HTTP/2 streams reset after 1 minute of silence
  • streaming responses to requests with a body, which were cut after 60 s

I reproduced that last one with an SSE stream opened with POST that emits one event every 10 s for 100 s. Version 2.11.6 cut it at 60 s and curl exited with code 18 (partial transfer); 2.11.4 and 2.11.7 completed it. As of 18 October 2026, 2.11.7 is the latest release: there is no 2.11.8 or 2.12 tag in the repository, and caddy:2, caddy:2.11 and caddy:latest point to it.

How to upgrade Caddy in Docker without surprises

Order matters, because expected_dot_headers and read_body_idle do not exist in 2.11.4. If you add those options before switching the image, 2.11.4 will not start:

Error: adapting config using caddyfile: parsing caddyfile tokens
for 'servers': unrecognized servers option 'expected_dot_headers'

max_header_size already existed and 2.11.4 accepts it, so you can add it first. For the rest, validate the new Caddyfile with the new image before you touch the running container:

docker run --rm \
  -v "$PWD/Caddyfile":/etc/caddy/Caddyfile:ro \
  caddy:2.11.7 caddy validate \
  --config /etc/caddy/Caddyfile --adapter caddyfile

If it prints Valid configuration, change the tag in your compose.yml to caddy:2.11.7 and recreate the service with docker compose up -d. If you stay on the same image and only change the Caddyfile, docker compose exec caddy caddy reload --config /etc/caddy/Caddyfile --adapter caddyfile applies it without a restart. If Caddy sits in front of other services, as in the guide to Forgejo with Docker and Caddy, test a real login afterwards: that is where large cookies show up.

People who install Caddy with apt also hit two problems unrelated to the code. On 30 September, apt rejected the Cloudsmith repository with EXPKEYSIG 531A6B20FA058A70 because it signed with a subkey that expired in 2024; Cloudsmith fixed it on 1 October (issue 8095).

Between 9 and 11 October, the same repository returned 402 Payment Required after exhausting its traffic quota (dist 142). A maintainer mentioned IPs with about 560,000 hits a month from continuous integration systems. Neither affects the Docker image.

Frequently asked questions

Can I go back to the old 1 MB limit?

Yes: max_header_size 1MB under servers restores the 2.11.4 behaviour. I would not do it unless you need it, because the low limit protects you from requests with huge headers. In my test, 64 KiB covered 12 cookies of 3,500 bytes, more than the cookies of the apps I run under one domain add up to.

Am I affected if Traefik or Nginx Proxy Manager sits in front of Caddy?

The limit applies in the Caddy that receives the request. If another proxy, such as Traefik or Nginx Proxy Manager, forwards to Caddy, the headers arrive unchanged and the 431 comes from Caddy. That proxy also has its own limit, so check both.

Why is the 431 missing from Caddy’s access log?

Because Go’s HTTP server answers before it hands the request to Caddy, so there is no access or error entry. To confirm that size is the cause, repeat the request with curl -i and a large header, or compare the response with 2.11.4.

Conclusion

Caddy 2.11.6 changed four defaults in a patch release, and each one is fixed with a line in the Caddyfile. max_header_size restores headroom for large cookies and tokens, expected_dot_headers brings back the dotted headers you need, and read_body_idle or the timeouts directive extends the upload deadline where it matters. client_auth has to be repeated in every block that should ask for a certificate. Go straight to 2.11.7, validate the Caddyfile with the new image before switching, and test a real login after the change.

Sources: [1] Caddy, v2.11.6 release notes[1], [2] Caddy, v2.11.7 release notes[2], [3] Caddy documentation, global options and the servers block[3], [4] Caddy documentation, timeouts directive[4], [5] Caddy, PR 7920 on client_auth and wildcards[5], [6] Go, net/http server.go and the header read margin[6], [7] Caddy, issue 8095 on EXPKEYSIG in the apt repository[7], [8] caddyserver/dist, issue 142 on 402 Payment Required[8], [9] MDN, 431 status code[9].

Sources

  1. Caddy, v2.11.6 release notes
  2. Caddy, v2.11.7 release notes
  3. Caddy documentation, global options and the servers block
  4. Caddy documentation, timeouts directive
  5. Caddy, PR 7920 on client_auth and wildcards
  6. Go, net/http server.go and the header read margin
  7. Caddy, issue 8095 on EXPKEYSIG in the apt repository
  8. caddyserver/dist, issue 142 on 402 Payment Required
  9. MDN, 431 status code