496 SSL Certificate Required
Client error response defined by nginx.org.
Last updated July 27, 2026
Common causes at a glance
- A client with no client certificate configured for an mTLS-only route
- A Kubernetes secret or file mount missing so the pod has no cert to send
- A browser or tool that cannot present client certificates hitting the endpoint
- ssl_verify_client optional applied to a location that should have been open
Reproduce it
curl -s -o /dev/null -w '%{http_code}\n' https://mtls.example.com/api/healthWhat 496 SSL Certificate Required means
The difference between 495 and 496 is presence, not validity. 496 means the
client completed the TLS handshake without supplying a certificate to a location that
requires one. As with 495, you only see it when verification is deferred with
ssl_verify_client optional; under on nginx aborts during the
handshake and the client gets a TLS alert instead.
Splitting the two codes is useful. 496 almost always means a caller was never configured for mTLS — wrong environment, missing secret mount, a tool that cannot present certificates. 495 means someone tried and got it wrong.
Where it comes from
Like its neighbours, 496 has no RFC and no IANA registration. It is listed in
the nginx SSL module’s error processing section as a non-standard code for use with
error_page, meaning a client has not presented the required certificate; the
trust anchors come from
ssl_client_certificate.
Under RFC 9110 §15 an unrecognised 4xx is treated as 400, so no
client reads 496 as “retry with a certificate”. If you want callers to react, return a
documented body from your error_page handler and keep 496 for logs, where it
separates unconfigured clients from broken credentials.
What actually causes it
Deployment gaps dominate. A service works in staging where mTLS is off,
then fails in production because the certificate comes from a secret nobody created in
that namespace, or sits at a path the process does not read. Health checks are the other
reliable source: probes and uptime monitors rarely carry client certificates, so they
generate a steady baseline of 496 against a healthy service and make the error rate look
alarming. Browsers are third — a human opening an mTLS URL without an installed
certificate gets 496 and, unless you wrote an error_page, no explanation of
what to install.
How to debug it
First separate “no certificate” from “bad certificate”; the fixes are unrelated. Request without one and read the code:
curl -s -o /dev/null -w '%{http_code}\n' https://mtls.example.com/api/health
496 there and 200 with --cert confirms the endpoint is behaving and the
caller is unconfigured. Check the server really is requesting a certificate by looking
for the acceptable CA names block in
openssl s_client -connect mtls.example.com:443; if it is absent,
ssl_verify_client is off for the server block you reached.
For health checks, carve out the probe path with ssl_verify_client off;
rather than issuing certificates to every monitoring agent. Use the
TLS inspector to confirm which server block a given SNI value
lands on.
Working examples
curl
# No client certificate: expect 496 (or 400 if nginx does not expose it).
curl -s -o /dev/null -w '%{http_code}\n' https://mtls.example.com/api/health
# With a valid certificate the same URL should return 200.
curl -s -o /dev/null -w '%{http_code}\n' \
--cert client.pem --key client.key https://mtls.example.com/api/health
# Does the server even ask for a certificate?
openssl s_client -connect mtls.example.com:443 </dev/null 2>&1 \
| grep -A5 'Acceptable client certificate CA names'
Python (requests)
import requests
url = "https://mtls.example.com/api/health"
# No cert argument: nginx sees an empty certificate list.
print("without cert:", requests.get(url, timeout=10).status_code) # 496
# With one, the same request should succeed.
print("with cert:", requests.get(
url, cert=("client.pem", "client.key"), timeout=10
).status_code)
# Health checkers usually look like the first call. Exempt the probe path
# in nginx rather than shipping certificates to every monitoring agent.
Node (fetch)
import { readFileSync } from "node:fs";
import { Agent } from "undici";
const url = "https://mtls.example.com/api/health";
// Plain fetch never sends a client certificate.
console.log("without cert:", (await fetch(url)).status); // 496
const agent = new Agent({
connect: {
cert: readFileSync("client.pem"),
key: readFileSync("client.key"),
},
});
console.log("with cert:", (await fetch(url, { dispatcher: agent })).status);Reference and tooling
The authoritative list of assigned status codes is theIANA HTTP Status Code Registry. For a searchable copy with the semantics summarised, use theHTTP status code reference on this site. To see exactly what a server is sending, theheader inspector shows the raw status line and response headers, and thecurl builder assembles the request flags for you.
Other 4xx codes
- 400 Bad Request
- 401 Unauthorized
- 402 Payment Required
- 403 Forbidden
- 404 Not Found
- 405 Method Not Allowed
- 406 Not Acceptable
- 407 Proxy Authentication Required
- 408 Request Timeout
- 409 Conflict
- 410 Gone
- 411 Length Required
- 412 Precondition Failed
- 413 Content Too Large
- 414 URI Too Long
- 415 Unsupported Media Type
- 416 Range Not Satisfiable
- 417 Expectation Failed
- 418 (Unused)
- 419 Page Expired
- 421 Misdirected Request
- 422 Unprocessable Content
- 423 Locked
- 424 Failed Dependency
- 425 Too Early
- 426 Upgrade Required
- 428 Precondition Required
- 429 Too Many Requests
- 431 Request Header Fields Too Large
- 444 No Response
- 451 Unavailable For Legal Reasons
- 494 Request Header Too Large
- 495 SSL Certificate Error
- 497 HTTP Request Sent to HTTPS Port
- 499 Client Closed Request