Skip to content

Request size limits

Severity: lowApplies to: Flask 3.1+Applies to: Flask 3.1.3Facts last verified 2026-08-13 against Flask 3.1.3 · Werkzeug 3.1.8

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.

settings on this page
MAX_CONTENT_LENGTH

Total 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
Read in: Flask 3.1.3 app.config, read from a running app
MAX_FORM_MEMORY_SIZE

Cap 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
Read in: Flask 3.1.3 app.config, read from a running app
MAX_FORM_PARTS

Cap 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
Read in: Flask 3.1.3 app.config, read from a running app
the fix
app.config["MAX_CONTENT_LENGTH"] = 16 * 1024 * 1024 # 16 MB

Werkzeug 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."}, 413
verify it workedRun this in: http response
Terminal window
head -c 50000000 /dev/zero > /tmp/big.bin
curl -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.bin

413 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:

Terminal window
python3 -c "
from app import app
for 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.

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.

before you ship this

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:

Terminal window
awk '{print $10}' access.log | sort -rn | head -20

Set 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.