Guide
Why do large uploads to Nextcloud fail behind a reverse proxy?
Because one layer between the client and Nextcloud has a request size or time limit, and which one depends on how the client uploads. We pushed files from 99 MiB to 5 GiB through nginx, Caddy and Traefik in front of Nextcloud 35 and recorded which layer answered each time.
Why do large uploads to Nextcloud fail behind a reverse proxy?
A layer in the path has a request body limit smaller than what the client sends. The web UI and desktop client split files into chunks of 100 MiB (104,857,600 bytes), so only one chunk has to pass each limit. curl, older rclone and other WebDAV tools send the whole file in one PUT, which has to pass every limit at once: nginx's 1 MB default, Nginx Proxy Manager's 2000 MB, Cloudflare's 100 MB on Free and Pro, and the official Docker image's 1 GiB Apache limit (APACHE_BODY_LIMIT). Read the 413 response body to see which layer sent it.
Because one layer between the client and Nextcloud refuses a request that big, or that slow. Which layer it is depends on how the client uploads. The web UI and the desktop client cut a large file into chunks of 100 MiB (104,857,600 bytes), so only one chunk has to get through. curl and other plain WebDAV tools send the whole file in one request, so the whole file has to get through every layer at once.
Chunked upload or single PUT: the difference that decides the fix#
A request body limit is the largest upload a server or proxy accepts in one HTTP request. Above it, the layer answers 413 Request Entity Too Large and never passes the request on.
A single PUT is one WebDAV request that carries the whole file. curl does this, and so do most generic WebDAV clients.
A chunked upload is Nextcloud's own method. The client creates a temporary folder, sends the file in numbered pieces, one request each, and then sends a MOVE that tells the server to join them. The chunk size comes from files.chunked_upload.max_size, which the admin manual says defaults to 104857600.
Who uses which:
- Web UI. Chunks any file bigger than the chunk size. Its upload library reads the server's value, so the default is 100 MiB. A smaller file goes as one PUT.
- Desktop client. Starts at 100 MiB and adjusts the size during an upload. Its docs say a maximum set by the server takes precedence.
- rclone. Chunks since v1.63.0, with a default of 10 MiB (
--webdav-nextcloud-chunk-size). - curl, scripts, most WebDAV mounts. One PUT per file.
This is why forum threads go in circles. One person raises a limit and the web UI starts working. Someone else raises the same limit and their curl or WebDAV upload still fails. Both are right about their own client.
A worked example: one 1.5 GiB video, two clients#
Take Nextcloud in the official nextcloud:apache image, behind Nginx Proxy Manager, with every setting at its default.
From the web UI. The file is cut into 15 chunks of 104,857,600 bytes and a smaller last one. NPM's nginx.conf sets client_max_body_size 2000m, and the image's Apache allows 1 GiB per request. Each chunk is far below both, so the upload goes through.
From curl or an old rclone. The file goes as one request of 1,610,612,736 bytes. That is under NPM's 2000 MiB, so NPM passes it on. Apache then refuses it, because it is over 1,073,741,824 bytes. The reply carries Server: nginx, because nginx does not pass a backend's Server header to the client. So the error looks like it came from the proxy. The body says otherwise: "The requested resource does not allow request data with PUT requests". That text is Apache's.
The fix for this case is in the Nextcloud container, not the proxy: APACHE_BODY_LIMIT=0. If the same file is ever over 2000 MiB, NPM refuses it too, and then the proxy needs a higher limit as well.
Put Cloudflare's proxy in front of the same setup and the picture changes. Cloudflare documents a cap of 100 MB per request on Free and Pro. The single PUT is refused at the edge before it reaches your server. The chunked upload is close to the line, because a default chunk is 104,857,600 bytes. Lower the chunk size and the question goes away.
What answered, measured#
We ran Nextcloud 35.0.2 in nextcloud:apache with its defaults: PHP_UPLOAD_LIMIT=512M and APACHE_BODY_LIMIT=1073741824. In front of it went nginx 1.30.6, Caddy 2.11.7 and Traefik 3.7.15. We sent single PUTs with curl. For chunks we sent the requests the web UI's library sends: MKCOL, numbered chunk PUTs and a final MOVE. We did not drive a browser.
| Proxy and setting | 101 MiB single PUT | 1,100 MiB single PUT | 5 GiB single PUT | Chunked, 100 MiB chunks |
|---|---|---|---|---|
| nginx, default 1 MB | 413 from nginx, already at 99 MiB | 413 from nginx | not run | 413 from nginx on chunk 1 |
nginx, client_max_body_size 100m | 413 from nginx | 413 from nginx | not run | 1,100 MiB and 5 GiB both arrived |
nginx, 2000m (NPM's default) | arrived | 413 from Apache, Server: nginx | 413 from nginx | 5 GiB arrived |
| Caddy, no limit set | arrived | 413 from Apache | 413 from Apache | not run |
Caddy, request_body max_size 100MB | 413 from Caddy | 413 from Apache | not run | 413 from Caddy on chunk 1 |
| Traefik, defaults | arrived | 413 from Apache | 413 from Apache | 5 GiB arrived |
Traefik, buffering maxRequestBodyBytes: 104857600 | 413 from Traefik | 413 from Traefik | not run | 1,100 MiB arrived |
Caddy, with APACHE_BODY_LIMIT=0 | not run | arrived (6.1 s) | arrived (22.2 s) | not run |
Three results are worth reading twice.
Exactly 104,857,600 bytes passes a 100 MiB limit. nginx with 100m and Traefik with 104857600 both accepted a single PUT and a chunk of exactly that size. 101 MiB was refused.
Caddy's 100MB is not 100 MiB. Caddy refused the 104,857,600-byte chunk, because MB there means a million bytes. The same Caddy accepted chunks of 100,000,000 bytes, and the 1,100 MiB file arrived in 12 of them. Write 100MiB in Caddy if you mean it.
PHP's limit never came into it. With APACHE_BODY_LIMIT=0, a 5 GiB PUT arrived while PHP_UPLOAD_LIMIT was still 512M. The admin manual agrees: upload_max_filesize and post_max_size "may not apply" to single PUTs or chunked uploads.
Behind nginx with 2000m, the 1,100 MiB PUT got Apache's 413 in 9 of 10 runs, after about 2 s. nginx first wrote the whole body to a temporary file, and its log said so: "a client request body is buffered to a temporary file". Only then did it pass the request to Apache, which refused it. In the tenth run Nextcloud answered instead, with a 400 saying it had read 0 bytes.
Joining chunks takes time too. The final MOVE for 5 GiB took 16.5 to 20.1 s on this host's local SSD. Slow disks take longer, and the proxy waits for that reply. The admin manual calls fastcgi_read_timeout "often the solution to 504 timeouts during MOVE transactions that occur even when using chunking".
Uploads that fail at 60 seconds: a time limit, not a size limit#
Some uploads die on the clock instead. Traefik's entry points have a readTimeout of 60 s, and its docs describe that as "the maximum duration for reading the entire request, including the body". We limited curl to 1 MB/s. A 101 MiB PUT through Traefik got 499 Client Closed Request after 60.06 s. So did a single 100 MiB chunk. The same 101 MiB PUT took about 101 s through Caddy and through nginx, and arrived both times.
On a home connection with 8 Mbit/s of upload, a 100 MiB chunk takes longer than a minute. Chunking does not save you here, because the limit applies to each request. Set the entry point's timeout to 0 (off) or to a long value:
entryPoints:
websecure:
address: ":443"
transport:
respondingTimeouts:
readTimeout: 0With readTimeout=0 the same slow chunk arrived. Caddy has no total upload timeout by default, but its docs give read_body_idle a default of 1m for an upload that stalls. nginx's client_body_timeout of 60 s also counts only the gap between two reads, not the whole upload.
How to tell which layer sent the 413#
Send one file with curl and look at the headers and the body:
curl -sS -D - -u alice:app-password -T test-1100M.bin \
https://cloud.example.com/remote.php/dav/files/alice/test-1100M.bin| What you see | Who answered |
|---|---|
HTML with <center>nginx/1.30.6</center> | nginx or NPM, client_max_body_size |
HTML with "does not allow request data with PUT requests" and an Apache/2.4.x address line | Apache in the Nextcloud image, LimitRequestBody |
Empty body, Server: Caddy | Caddy, request_body max_size |
Plain text "Request Entity Too Large", no Server header | Traefik buffering middleware |
499 Client Closed Request after 60 s | Traefik readTimeout |
XML with Sabre\DAV\Exception | Nextcloud itself; the request got through every layer |
Never trust the Server header behind nginx. nginx's docs say it does not pass a backend's Server field to the client, so even Apache's own 413 arrives labelled nginx.
The fix, layer by layer#
| Layer | Default limit | Fix |
|---|---|---|
| nginx | client_max_body_size 1m | client_max_body_size 10G; in the server block |
| Nginx Proxy Manager | 2000m in its nginx.conf | client_max_body_size 10G; in the host's Advanced tab |
| Caddy | none | nothing, or write MiB if you set request_body max_size |
| Traefik | no size limit, readTimeout 60 s | readTimeout: 0 on the entry point |
| Nextcloud image (Apache) | APACHE_BODY_LIMIT=1073741824 | APACHE_BODY_LIMIT: "0" in compose |
| Cloudflare proxy, Free and Pro | 100 MB per request | 50 MiB chunks, or a DNS-only record for big single PUTs |
The image's README says 0 means unlimited for APACHE_BODY_LIMIT, and the change applies when the container is recreated. Apache only got this limit in version 2.4.54. Up to 2.4.53 it was unlimited, which is why older guides never mention it.
For nginx, the example proxy config in the nextcloud/docker repository sets client_max_body_size 10G; and proxy_request_buffering off;. nginx's docs say that with buffering on, the entire request body is read from the client before the request goes to the backend. That is the temporary file from our log. Turning it off streams the upload instead.
In Nginx Proxy Manager, the 2000m lives in the shared nginx.conf. Put client_max_body_size 10G; in the proxy host's Advanced tab to raise it for that host only.
For Cloudflare, set the chunk size on the server and both the web UI and the desktop client follow it:
docker compose exec -u www-data app php occ config:system:set \
--type int --value 52428800 files.chunked_upload.max_sizeCloudflare's docs offer three ways past the cap: smaller chunks, a DNS-only record, or a higher plan. Pangolin vs Cloudflare Tunnel compares a tunnel you host yourself, where your own proxy sets the limit.
When this answer stops applying#
- The FPM image. With
nextcloud:fpmbehind your own nginx, the FastCGI and PHP-FPM time limits join the chain. The admin manual'sfastcgi_read_timeoutadvice is aimed at that setup. We tested only the Apache image. - Cloudflare itself. We did not send traffic through Cloudflare. The cap and the 413 come from its docs. We did not test whether a chunk of exactly 104,857,600 bytes passes its "100 MB".
- Object storage as primary storage. Chunks are staged on local disk first. Nextcloud server issue 29589 describes a 26 GB upload filling a small
/tmp. - The macOS client with virtual files. If only that client fails above about 100 MB while the web UI works, that matches desktop issue 9883, which was still open when we checked. That is a client bug, not a proxy limit.
- Slow storage. Our
MOVEtimings come from a local SSD and will not carry over to a NAS or a USB disk.
What to do next#
Send one test file with curl and read the 413 body to find the layer. Fix that layer, then repeat until Nextcloud's own XML answers. If your users only use the web UI and the desktop client, a proxy limit of 101 MiB or more is enough. If anyone uses curl, WebDAV mounts or an older rclone, raise every layer, including APACHE_BODY_LIMIT. Reverse proxy and TLS covers the rest of the proxy setup. Nextcloud "Access through untrusted domain" behind a reverse proxy fixes the trusted domain and http:// redirect problems that usually turn up in the same week. If the proxy answers 502 or 504 instead of 413, Why Nginx Proxy Manager returns 502 Bad Gateway reads the log line for you.
Questions#
Why does the Nextcloud web UI upload big files fine but curl or WebDAV fails?
The web UI cuts files bigger than the chunk size into 100 MiB pieces and sends each piece as its own request. curl sends the whole file as one request. In our lab, a 1,100 MiB file went through nginx with client_max_body_size 100m in 11 chunks, and the same file as one PUT got 413 from nginx in 5 ms. Raise the limit on every layer for single PUTs, or use a client that chunks.
Does PHP_UPLOAD_LIMIT fix Nextcloud uploads over 512 MB?
Not for WebDAV or chunked uploads. The admin manual says upload_max_filesize and post_max_size may not apply to single PUT requests or chunked uploads. In our lab the image kept its default PHP_UPLOAD_LIMIT of 512M, and a 5 GiB PUT still succeeded once APACHE_BODY_LIMIT was 0. The limit that stops a single PUT at 1 GiB in the official image is Apache's LimitRequestBody.
Why do Nextcloud uploads fail after exactly 60 seconds behind Traefik?
Traefik's entry points have a read timeout of 60 seconds for the whole request, body included. Any one request that takes longer is cut off. In our lab a 100 MiB chunk sent at 1 MB/s got 499 Client Closed Request after 60.06 s. Setting transport.respondingTimeouts.readTimeout=0 on the entry point let the same chunk through.
Can I upload files over 100 MB to Nextcloud through Cloudflare?
Through the web UI and desktop client, yes, if each chunk stays under the cap. Cloudflare documents 100 MB per request on Free and Pro and 200 MB on Business, and answers 413 above it. Set Nextcloud's chunk size to 50 MiB so every chunk sits well below. A single WebDAV PUT over 100 MB cannot pass on Free or Pro. Use a DNS-only record for those uploads, as Cloudflare itself suggests.
Does rclone use chunked uploads with Nextcloud?
Yes, since v1.63.0, which added Nextcloud chunked uploads. Its WebDAV docs list --webdav-nextcloud-chunk-size with a default of 10 MiB, and 0 turns chunking off. An older rclone, or chunking set to 0, sends each file as a single PUT, so the whole file has to pass the proxy and Apache's limit.
What is the default Nextcloud chunk size?
100 MiB, which is 104,857,600 bytes. It is set by files.chunked_upload.max_size and shown in the server's capabilities. Change it with occ config:system:set --type int --value 52428800 files.chunked_upload.max_size. The web UI reads this value. The desktop client starts at 100 MiB and adjusts its chunks during an upload, but its docs say a maximum from the server takes precedence.
Sources#
- Nextcloud admin manual, uploading big files
- nextcloud/docker README (PHP_UPLOAD_LIMIT, APACHE_BODY_LIMIT)
- nextcloud/docker, 35/apache Dockerfile (LimitRequestBody)
- nextcloud-upload, lib/utils/config.ts (web UI chunk size)
- Nextcloud desktop client, advanced configuration (chunkSize)
- Cloudflare docs, upload limits by plan
- nginx documentation, client_max_body_size and proxy_hide_header
- Nginx Proxy Manager, nginx.conf (client_max_body_size 2000m)
- Traefik documentation, entry points (readTimeout)
- Caddy documentation, request_body
- rclone documentation, WebDAV backend
Published . Last reviewed . Found something out of date? Tell us and we will fix it and log the change.