Skip to content
DERKONLINE

Why Your Site Returns 520 in Safari but Works in Chrome

Safari coalesces cookies into one HPACK field that trips nginx 1.18 limits, so Cloudflare returns an untraceable 520. Two lines fix it.

Derrick S. K. Siawor7 min read

A customer messages you that your site is broken. You open it in Chrome and it works perfectly. You ask which browser they use. Safari. You open Safari, and there it is: a Cloudflare 520 error on the exact page they described, usually a POST or a form submit. The page that works flawlessly in one browser is dead in another, and Cloudflare is reporting a server error you cannot find anywhere in your logs.

The Safari-only Cloudflare 520 happens because Safari packs all cookies into one HTTP/2 header field, and when large auth cookies push that field past nginx 1.18's default 4KB http2_max_field_size, nginx silently resets the stream and Cloudflare returns 520. Raise http2_max_field_size to 32k and http2_max_header_size to 64k in the affected server block, and the error disappears.

This is one of the most frustrating bugs in the modern web stack, because every instinct points you the wrong way. It looks like a TLS problem, a firewall problem, a Cloudflare problem. It is none of those. It is a collision between large authentication cookies, how Safari encodes HTTP/2 headers, and a default limit in older nginx. Once you know the shape of it, the fix takes two minutes. Getting there without knowing the shape can burn an entire day.

The symptom pattern, exactly

Here is how to recognize it fast, because recognizing it is most of the battle:

  • The site works in Chrome but fails in Safari, or any browser that handles cookies the way Safari does.
  • Cloudflare returns a 520, with Server: cloudflare and an HTML body, on POST or other request types that carry cookies.
  • Your nginx access log shows status 000 entries from Cloudflare IPs. The connection was accepted, then aborted before nginx received a complete HTTP request.
  • Your nginx error log is silent for those same timestamps. No 4xx, no 5xx logged, because nginx never got far enough to log an HTTP-level error. This is exactly the kind of failure that hides from you unless you have turned noisy server logs into alerts you actually trust and are watching for 000-status entries rather than HTTP errors.
  • In the browser, a fetch() sees the 520 and the app's res.json() throws, because the body is HTML, so your catch block fires and the user sees a generic "something went wrong."
  • The app has large session cookies. Supabase auth tokens chunked into sb-...-auth-token.0 and .1, each several kilobytes, or Auth0 SDK cookies, or NextAuth JWE cookies. Anything that produces a big Cookie header.

If that list matches what you are seeing, stop looking at TLS and firewalls. The cause is the header size. This is the same root cause behind the Safari-only 520 error that large auth cookies quietly cause, seen from the Cloudflare side.

Why Safari specifically

Safari-only Cloudflare 520 root cause: coalesced Cookie field exceeds nginx 4k, stream reset, fix to 32k

The mechanism comes down to how the two browsers pack cookies into HTTP/2.

HTTP/2 compresses headers with HPACK. The spec permits a browser to send all cookies as one combined Cookie header field, and it equally permits splitting them across multiple Cookie fields. Both are legal. Chrome splits cookies into multiple fields. Safari coalesces every cookie into a single Cookie field.

That difference is the whole bug. When your auth cookies are large, Safari's single combined Cookie field can exceed a per-field size limit at the origin. Chrome's split fields each stay under it. So the same cookies, the same site, the same request produce a request that nginx accepts from Chrome and rejects from Safari.

Older nginx, specifically the 1.18 line still common on Ubuntu LTS boxes, has two defaults that bite here:

  • http2_max_field_size 4k caps a single HPACK header field after decompression.
  • http2_max_header_size 16k caps the total decompressed headers.

When Safari's coalesced Cookie field crosses 4 kilobytes, nginx 1.18 silently resets the HTTP/2 stream. It does not log an HTTP error because there is no complete HTTP request to log against. Cloudflare, sitting in front, sees an empty or aborted origin response and returns a 520 to the user. That is the 000 in your access log and the silence in your error log, fully explained.

The fix

For nginx 1.18, raise the two limits inside each affected server { } block:

server {
    # ... your existing config ...
    http2_max_field_size 32k;
    http2_max_header_size 64k;
}

Putting it in the server block rather than http keeps the change scoped so it does not affect sibling vhosts. Reload nginx, retest in Safari, and the 520 is gone.

One important version note: nginx 1.19.7 and later removed those two directives entirely. On those versions, large_client_header_buffers 4 16k; covers both HTTP/1.1 and HTTP/2, so you set that instead. If you copy the 1.18 directives onto a newer nginx, it will fail to start with an unknown-directive error. Check your version first with nginx -v.

How to confirm it before touching anything

Do not guess. The fastest confirmation lives in the browser, and getting it takes seconds. Ask the affected user (or use your own Safari) to open Safari's Web Inspector, go to the Network tab, click the failing request, and look at the Headers panel. The request Cookie header size is shown right there. Count the bytes. If the coalesced Cookie field is over 4 kilobytes and you are on nginx 1.18 behind Cloudflare, you have your answer before changing a single line of config.

This is the discipline that saves the day: when a Cloudflare-fronted site has a browser-specific 5xx, get the browser's Network tab first. The cookie size is visible there in seconds, and it tells you whether to touch the server at all. Chasing TLS session caches, firewall rules, and CDN settings before reading the request headers is how this bug eats an afternoon.

Two ways to prevent it for good

Raising the nginx limits fixes the immediate failure. Two changes prevent the whole class:

  1. Bake the override into your provisioning. Any new nginx 1.18 box that sits behind Cloudflare and uses chunked auth cookies should ship with http2_max_field_size and http2_max_header_size already raised. Do not rely on rediscovering this per incident. Put it in the setup script so the server is correct from the first deploy.
  2. Keep your cookies smaller. Large auth cookies are the underlying cause. If your session library chunks a multi-kilobyte token across cookies, consider a server-side session store that keeps only a small session id in the cookie and holds the heavy payload in Redis or your database. A short cookie never approaches any of these limits, in any browser, on any nginx version. The same compact-token discipline pairs well with issuing JWTs attackers cannot forge or replay, where the payload stays lean by design.

The broader lesson

This bug is a specimen of a pattern worth internalizing: when something works in Chrome and breaks in Safari, Safari is usually the strict one, enforcing a spec behavior the other browser happens to sidestep. It applies here with HPACK field coalescing, and it applies elsewhere too, with HSTS and upgrade-insecure-requests breaking your dev site in Safari but not Chrome on localhost, and with Next.js 15 pages that render unstyled only in Safari. The reflex to build is "Chrome-works-Safari-broken means check what Safari does differently at the protocol level," not "Safari is buggy."

Tuning origin servers so they behave correctly behind a CDN, across every browser, is the unglamorous core of running real production sites. It is the same kind of CDN-edge tuning that lets you push your cache hit ratio past 95 percent instead of leaking requests to the origin. It is the kind of problem we fix daily in server administration and surface proactively in networking work, long before a customer has to tell you their browser is the one that breaks. The next time a 520 only shows up in Safari, you already know where to look: the Network tab, the cookie size, and two lines of nginx config.