What the error means
When a server sends Content-Length: 500000, it is promising that the body is exactly 500,000 bytes. The browser counts the bytes as they arrive. If the connection closes after 410,000, the browser knows the response was cut short, throws it away, and reports ERR_CONTENT_LENGTH_MISMATCH (Chromium net error -354).
You usually see it in the DevTools console as:
Failed to load resource: net::ERR_CONTENT_LENGTH_MISMATCH
What you see on the page depends on what was cut: an image renders broken or half-drawn, a script or stylesheet doesn’t apply, a download stops partway.
So this is a truncation error, not a connection error. DNS worked, the connection opened, the headers arrived, the body started flowing. Then something stopped it. The question is never “why can’t I connect” but “what stopped the body before the end”.
Three details that save time:
- Too many bytes is not this error. If the body is longer than Content-Length, the browser simply stops reading at the advertised length. That bug gives silently truncated content, not an error.
- Chunked responses fail under a different name. If the server sends
Transfer-Encoding: chunkedinstead of a length and the body is cut off, Chrome reportsERR_INCOMPLETE_CHUNKED_ENCODING. Same causes, different framing. Which one you get depends on whether a length was sent before things went wrong. - Check the protocol. In DevTools → Network, enable the Protocol column. This error is mostly seen on HTTP/1.1 connections. On HTTP/2 and HTTP/3, a cut-off body tends to surface as a protocol error (
ERR_HTTP2_PROTOCOL_ERRORand similar) instead, which is worth knowing when a CDN speaks HTTP/2 to browsers and HTTP/1.1 to your server.
If you’re just visiting
There is little to fix on your side. Try the page on another network (your phone on mobile data), and turn off any VPN, proxy or antivirus web scanning, since those sit in the middle of the connection and can cut it. A hard reload (Ctrl+Shift+R, or Cmd+Shift+R on a Mac) rules out a half-cached copy.
If it fails on every network, the site is sending incomplete responses and only its owner can fix it. Telling them which URL failed and at what time helps them find it in their logs.
Measure it with curl
Reproduce the exact request and compare the declared length with what actually arrived. On curl 7.84 or newer:
curl -sS --http1.1 -o /dev/null -H 'Accept-Encoding: gzip, br' \
-w 'status=%{http_code} declared=%header{content-length} received=%{size_download}\n' \
https://example.com/assets/app.js
When the body is cut short, curl stops with its own version of the error:
curl: (18) transfer closed with 183402 bytes remaining to read
For a chunked response the message is transfer closed with outstanding read data remaining instead.
Two parts of that command matter. --http1.1 keeps the test on the protocol where this error lives. The Accept-Encoding header makes the server behave as it does for a browser, which asks for compression. Without --compressed curl doesn’t decompress anything, so size_download counts the raw bytes on the wire, which is exactly what Content-Length describes.
If the failure is intermittent, loop it:
for i in $(seq 20); do
curl -sS --http1.1 -o /dev/null -H 'Accept-Encoding: gzip, br' \
-w '%{http_code} %{size_download} %{time_total}s\n' https://example.com/big.zip
done
A truncation that always happens at about the same elapsed time points to a timeout. One that happens at about the same byte count points to a buffer or temp file.
To find out which layer is cutting the body, repeat the test against the origin directly, bypassing the CDN or load balancer:
curl -sS --http1.1 -o /dev/null --resolve example.com:443:203.0.113.10 \
-w '%{size_download}\n' https://example.com/big.zip
The HTTP check shows from outside your network which headers the server sends for a URL: whether there is a Content-Length or Transfer-Encoding: chunked, and any Content-Encoding. It reads headers only, so use curl for the byte count.
The usual causes
1. nginx can’t write its proxy temp file
This is the classic cause when nginx sits in front of an application, and it explains the “only large files” pattern.
nginx buffers the upstream response in memory (proxy_buffers, or fastcgi_buffers for PHP-FPM). When a response doesn’t fit, nginx writes the rest to a temporary file under proxy_temp_path (or fastcgi_temp_path). If it can’t write that file, the client receives a response that stops short of the Content-Length nginx already sent.
Why it can’t write:
- Permissions. The temp directory is owned by a different user than the nginx worker processes. Typical after changing the
userdirective, after running nginx once as another user, or with a container image that ships the directories with the wrong owner. - A full disk, or a filesystem out of inodes.
The error log says so, in lines like:
open() "/var/lib/nginx/proxy/3/00/0000000003" failed (13: Permission denied) while reading upstream
or (28: No space left on device) for a full disk. Check:
nginx -V 2>&1 | tr ' ' '\n' | grep temp-path # compiled-in default paths
sudo nginx -T 2>/dev/null | grep -E '^\s*user|_temp_path|max_temp_file_size'
ps -o user= -C nginx | sort | uniq -c # which user the workers run as
sudo ls -ld /var/lib/nginx /var/lib/nginx/* # path differs between distributions
df -h /var/lib/nginx; df -i /var/lib/nginx
Fix: give the temp directories to the worker user (sudo chown -R www-data: /var/lib/nginx on Debian and Ubuntu; adjust the user and path to yours), or free the disk. If you’d rather nginx never use temp files for proxied responses, proxy_max_temp_file_size 0; turns them off; nginx then passes data on as fast as the client reads it, which keeps the upstream connection busy longer for slow clients.
2. The backend stopped mid-body
The headers, including Content-Length, went out. Then the process producing the body died or was stopped. nginx logs this as upstream prematurely closed connection while reading upstream.
Common versions:
- PHP-FPM
request_terminate_timeout. A script that streams a large file to a slow client gets killed when this wall-clock limit is reached. The FPM log shows a line containingexecution timed outandterminating. Note thatmax_execution_timeon Linux doesn’t count time spent in stream operations or system calls, so a slowreadfile()download is usually stopped byrequest_terminate_timeout, notmax_execution_time. - A crash or OOM kill halfway through generating a big export. Check
journalctl -u <service>andsudo dmesg -T | grep -i 'killed process'at the time of the failure. proxy_read_timeout(default 60 seconds) in nginx. It measures the gap between two reads from the upstream, not the whole response, so it fires when a backend stalls mid-stream, for example while it waits on a slow database query between chunks.
Fix: raise the specific limit for the endpoints that need it, or better, stop generating large responses inside the request. Write the export to storage and serve it as a static file.
3. The file changed while it was being sent
nginx serves a static file with Content-Length set from the file’s size at the start of the request. If a deploy overwrites that file in place during the transfer, or a log rotation truncates it, the remaining bytes may no longer exist. nginx logs an alert that the file was truncated.
Fix: make deploys atomic. Write new files under a temporary name and mv them into place (a rename on the same filesystem is atomic), or deploy into a new release directory and switch a symlink. Never copy over files that are being served.
A development-only variant: on VirtualBox and Vagrant shared folders, nginx’s sendfile is a known source of stale or truncated files. Set sendfile off; in that environment.
4. The Content-Length header is simply wrong
Something computed the length, and then something else changed the body:
- Application code sets Content-Length from the uncompressed size and then compresses the output (or a framework middleware compresses it afterwards).
- A PHP script sets
Content-Lengthfromfilesize()whilezlib.output_compressionor an output buffer handler compresses or alters the output. - A filter that rewrites HTML on the way out (injecting a script tag, minifying) without recalculating the header.
nginx’s own gzip module isn’t the problem here: it removes Content-Length when it compresses. The trouble is code that sets the header by hand.
Fix: don’t set Content-Length yourself. Let the web server or framework compute it from the final body, or let it send the response chunked.
The mirror-image bug is worth knowing because it produces no error at all. In Node.js, res.setHeader('Content-Length', body.length) on a string with non-ASCII characters declares too few bytes, because length counts UTF-16 code units, not UTF-8 bytes. The browser stops at the declared length and silently drops the end of your JSON or HTML. Use Buffer.byteLength(body).
5. Something between the server and the visitor
If the origin delivers the full body when tested with --resolve but the public URL doesn’t, look at the layers in between: a load balancer or CDN with an idle or response timeout shorter than your slowest download, or a proxy that buffers to a small disk. If the failure only happens for some visitors, their own network may be cutting it (a corporate proxy, antivirus that scans downloads), and there’s nothing to change on your server.
After the fix
- Re-run the curl loop above until twenty requests in a row return the full size.
- Test the largest real files you serve, not just the one that was reported. The limits that cause this scale with size.
- Watch the nginx error log for
while reading upstreamfor a day of real traffic.
Related errors
- ERR_EMPTY_RESPONSE: the connection closed before any response at all, not partway through the body.
- 502 Bad Gateway: the backend failed before nginx could send headers, so it could still report an error.
- 504 Gateway Timeout: a proxy timeout that fired before the response started.
- TTFB, compression and Cache-Control checklist: checking what Content-Encoding and caching headers your server actually sends.
Check your own domain now
Free, no sign-up. Runs the exact check this guide describes and shows what to fix.