Request size limits
Three settings govern how much a client may send, and they do not have the same default:
| Setting | Default | Covers |
|---|---|---|
MAX_CONTENT_LENGTH |
None |
The whole request body |
MAX_FORM_MEMORY_SIZE |
500000 |
Non-file form fields held in memory |
MAX_FORM_PARTS |
1000 |
Number of multipart parts |
So a form post is bounded and a file upload is not. MAX_FORM_MEMORY_SIZE caps the
non-file fields at 500 KB and MAX_FORM_PARTS caps the part count, but the total body —
which is where an upload lives — has no limit at all until you set one.
That split is worth knowing because it explains why testing with a large form field suggests the application is protected. It is, against that. It is not against a large file.
MAX_CONTENT_LENGTHTotal request body cap. None means unbounded, so a file upload has no ceiling out of the box — this is the one of the three size settings that ships unset, and the one that matters for uploads.
- accepts
None|any int, bytes- default
None- set in
app.config
MAX_FORM_MEMORY_SIZECap on non-file form fields held in memory. Set by default, which is exactly why testing your app with an enormous text field suggests you are protected when uploads still are not.
- accepts
500000|any int, bytes|None- default
500000- set in
app.config
MAX_FORM_PARTSCap on the number of multipart parts, bounding part-count floods rather than total size.
- accepts
1000|any int|None- default
1000- set in
app.config
app.config["MAX_CONTENT_LENGTH"] = 16 * 1024 * 1024 # 16 MBWerkzeug enforces this while reading, so an oversized request is rejected with 413
rather than being buffered and then judged. That is the property that makes it a real
control rather than an after-the-fact check.
Give the rejection a body, because the default is bare and support cannot distinguish it from a network failure:
from werkzeug.exceptions import RequestEntityTooLarge
@app.errorhandler(RequestEntityTooLarge)def too_large(e): limit = app.config["MAX_CONTENT_LENGTH"] // (1024 * 1024) return {"error": f"File too large. Maximum is {limit} MB."}, 413head -c 50000000 /dev/zero > /tmp/big.bincurl -s -o /dev/null -w '%{http_code} %{time_total}s\n' \ -X POST https://app.example.com/upload -F 'f=@/tmp/big.bin'rm /tmp/big.bin413 is correct, and it should return quickly. A slow 413 means the body was read
before being rejected, which is the exhaustion you were trying to avoid. A 200 means
there is no cap.
Read the config to see which of the three are set:
python3 -c "from app import appfor k in ('MAX_CONTENT_LENGTH','MAX_FORM_MEMORY_SIZE','MAX_FORM_PARTS'): print(f'{k:24}', app.config.get(k))"Per-route limits, since one number rarely fits
Section titled “Per-route limits, since one number rarely fits”MAX_CONTENT_LENGTH is global, and an application with a document upload and a JSON API
wants very different numbers for each. Since Flask 3.1 the limit can be tightened per
request:
from flask import request
@app.post("/api/items")def create_item(): request.max_content_length = 64 * 1024 # 64 KB for JSON data = request.get_json() ...Set the global limit to the largest legitimate upload and tighten it on the routes that should never see one. A JSON endpoint accepting 16 MB because the avatar upload needs to is a limit that is not doing much.
The proxy usually caps this first
Section titled “The proxy usually caps this first”Most reverse proxies impose a body limit by default, often around one megabyte, so the exposure may already be closed for traffic arriving through the proxy — and the symptom you meet first is the opposite one: legitimate uploads rejected before they reach Flask.
Two things follow. Check the edge before assuming the application is exposed. And when
an upload fails, work out which layer rejected it — a proxy error page looks nothing
like Flask’s 413, and that difference is the quickest way to tell.
The application limit still earns its place. It covers anything reaching the application port directly, and it keeps the number next to the code that knows what a reasonable size is.
A cap breaks the legitimate large upload, and the size that counts as legitimate is usually discovered after the limit ships — a video, a database export, a batch import that was always 40 MB and nobody mentioned.
Measure before choosing:
awk '{print $10}' access.log | sort -rn | head -20Set the cap above the observed maximum with headroom.
The rejection also lands earlier than most error handling expects. Because Werkzeug
aborts while reading the body, a 413 can reach the client before your view runs at
all — so a front end that only handles errors returned by the view will treat it as an
unexplained failure. Handle RequestEntityTooLarge explicitly, as above, and make sure
the client surfaces the message rather than treating any non-2xx as generic.
Related
Section titled “Related”- Serving files — the other half of handling uploads
- The production server — where the other request limits live