# Large uploads and long requests: 413/504 across IIS, Nginx and the app

> Size body limits, timeouts and buffering at every hop, and choose streaming over a 30-minute HTTP request.

**Tested on**

- Build: 26100.33650
- Edition: Standard / Datacenter
- Architecture: x64
- Evidence: E1
- Verified: 2026-10-29

## The short answer

A large upload crosses several limits:

| Hop | Limit |
|---|---|
| Browser/app | its own max body |
| IIS | `maxAllowedContentLength` (bytes) |
| Nginx | `client_max_body_size` |
| Proxy | ARR/proxy body limit |
| App | framework body limit |
| Time | HTTP.sys `requestTimeout`, Nginx `client_body_timeout`, proxy read timeout |

Raise the smallest one first; do not set everything to unlimited.

## IIS

```xml
<system.webServer>
  <security>
    <requestFiltering>
      <requestLimits maxAllowedContentLength="524288000" />
    </requestFiltering>
  </security>
</system.webServer>
```

`524288000` = 500MB. Also check the handler/app limit (ASP.NET `maxRequestLength`, ANCM request body limit) and `serverRuntime requestTimeout`.

For uploads, `uploadReadAheadSize` can matter with client certificates/large headers; do not raise it blindly.

## Nginx

```nginx
client_max_body_size 500m;
client_body_timeout 300s;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
```

`client_body_temp_path` needs disk space. For large streaming uploads, `proxy_request_buffering off` forwards bytes to the app instead of buffering the whole body on disk (tradeoff: the app must handle slow clients).

## App

Set the framework’s limit too:

- ASP.NET Core: `RequestSizeLimit`/Kestrel `MaxRequestBodySize`;
- Express: `express.json({ limit: '...' })` / multer limits;
- Flask: `MAX_CONTENT_LENGTH`;
- Tomcat: `maxPostSize`/connector limits.

If the app limit is lower, the proxy passes the body and the app rejects it (often a 413 from the app).

## Choose streaming for large files

A 2GB upload or a 30-minute export should not be a single synchronous HTTP request:

- direct-to-object-storage presigned upload;
- chunked/resumable upload;
- background job + status endpoint for exports;
- download via signed URL/stream with a bounded timeout.

> **Caution: Timeouts are not a fix for slow work**
>
> Raising `requestTimeout` to hours keeps a worker/connection occupied and makes deploys/restarts painful. Move long work to a queue and return a job id.

## Verify

```powershell
curl.exe -I -H "Content-Length: 600000000" https://lab.example/upload
curl.exe --limit-rate 100k -T big.bin https://lab.example/upload
```

Check the status at each hop and the IIS/Nginx logs. `413` is a body limit; `504` is a timeout; `502` is the upstream.

## Lab boundary

E1. The lab did not push a large upload through IIS/Nginx; the limits are the cross-hop checklist for Server 2025.

## Sources

- [Microsoft Learn — Request limits](https://learn.microsoft.com/en-us/iis/configuration/system.webserver/security/requestfiltering/requestlimits/)
- [nginx — core module](https://nginx.org/en/docs/http/ngx_http_core_module.html)
- [nginx — proxy module](https://nginx.org/en/docs/http/ngx_http_proxy_module.html)

---

Source: https://windows.homes/en/server/tls/large-files/
