AI & Agents

Flask MAX_CONTENT_LENGTH: File Upload Limits, 413 Errors, and Streaming

Flask MAX_CONTENT_LENGTH defines the maximum allowable byte size for incoming request bodies, returning an HTTP 413 error when clients send oversized payloads. While configuring this setting protects application memory, production architectures must coordinate reverse proxy buffers in Nginx, handle Werkzeug spooling thresholds, and adopt streaming or external workspace storage for large data sets.

Tom Langridge 19 min read Updated
Flask evaluates request body sizes against MAX_CONTENT_LENGTH before reading data, raising an HTTP 413 error when limits are exceeded.

How to Configure Flask MAX_CONTENT_LENGTH and Catch 413 Errors

According to official Flask Configuration Documentation, Flask sets MAX_CONTENT_LENGTH to None by default, meaning the framework itself imposes no cap on incoming request body bytes until configured.

Flask MAX_CONTENT_LENGTH is a configuration option that sets the maximum allowable byte size of an incoming HTTP request body, automatically rejecting oversized uploads with an HTTP 413 status code.

Without an explicit boundary in your application configuration, an unconstrained endpoint creates serious stability risks. A single client transmitting an uncompressed multi-gigabyte video or archive can overwhelm server RAM or exhaust container temporary storage. Setting a hard limit protects backend processes from memory exhaustion and denial-of-service transmissions.

Configuring the limit requires a single configuration key on your Flask application object. When an incoming request exceeds this value, Flask raises a RequestEntityTooLarge exception from Werkzeug's HTTP exception hierarchy:

from flask import Flask, jsonify, request
from werkzeug.exceptions import RequestEntityTooLarge

app = Flask(__name__)
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024  # 16 MB limit

@app.errorhandler(RequestEntityTooLarge)
@app.errorhandler(413)
def handle_payload_too_large(error):
    return jsonify({
        "error": "Payload Too Large",
        "message": "The uploaded payload exceeds the configured limit."
    }), 413

@app.route('/upload', methods=['POST'])
def upload_file():
    if 'file' not in request.files:
        return jsonify({"error": "No file part in request"}), 400
    uploaded_file = request.files['file']
    return jsonify({
        "filename": uploaded_file.filename,
        "status": "received"
    }), 200

How Flask Evaluates Incoming Request Sizes

Flask does not read the entire incoming socket before evaluating the payload size. Instead, it inspects the HTTP Content-Length header submitted by the client during request initialization.

When your route handler or middleware accesses request payload attributes like request.files, request.form, or request.data, Flask invokes Werkzeug's WSGI environment parser. If the client declared a Content-Length header whose integer value exceeds MAX_CONTENT_LENGTH, Flask raises RequestEntityTooLarge immediately. The underlying socket read terminates before your application allocates memory buffers for the body.

If an incoming request omits the Content-Length header and uses chunked transfer encoding (Transfer-Encoding: chunked), Flask cannot predict the total byte count in advance. In this scenario, Werkzeug streams data into its internal buffers chunk by chunk, tracking accumulated bytes. The moment the total received bytes cross MAX_CONTENT_LENGTH, Werkzeug halts further reading and raises the 413 exception.

When handling standard form posts, the default Werkzeug behavior generates a plain HTML error page. Registering an explicit error handler with @app.errorhandler(413) or @app.errorhandler(RequestEntityTooLarge) converts this failure into clean, structured JSON suitable for REST APIs and automated clients.

How to Configure Per-Route Content Length Limits

Global application limits apply across every route, which can become problematic when an application requires different thresholds for different endpoints. For example, user profile metadata forms may require a strict 2 MB ceiling, while a document ingestion route must accept up to 50 MB.

In standard Flask architectures, developers handle route-specific limits by setting a permissive global MAX_CONTENT_LENGTH (or leaving it unconfigured) and verifying file sizes inside specific view functions or decorators. Inspecting request.content_length before accessing request.files allows you to reject oversized payloads early:

from functools import wraps
from flask import abort, jsonify, request

def limit_content_length(max_bytes):
    def decorator(f):
        @wraps(f)
        def wrapper(*args, **kwargs):
            cl = request.content_length
            if cl is not None and cl > max_bytes:
                abort(413)
            return f(*args, **kwargs)
        return wrapper
    return decorator

@app.route('/api/avatar', methods=['POST'])
@limit_content_length(2 * 1024 * 1024)  # 2 MB limit
def upload_avatar():
    file = request.files['avatar']
    return jsonify({"status": "avatar uploaded"}), 200

@app.route('/api/document', methods=['POST'])
@limit_content_length(50 * 1024 * 1024)  # 50 MB limit
def upload_document():
    file = request.files['document']
    return jsonify({"status": "document uploaded"}), 200

Starting in Flask 3.1, the framework introduced per-request limits via Request.max_content_length. By subclassing Request or modifying the request instance inside a before_request hook, you can assign dynamic ceilings based on the request path or authenticated user permissions.

What Happens in Werkzeug During Multipart Uploads and Memory Spooling

Flask delegates all low-level HTTP parsing and multipart form processing to Werkzeug, the WSGI utility library developed by the Pallets project. Understanding how Werkzeug manages memory allocation during uploads clarifies what happens between the initial socket read and your route code.

When a client transmits a multipart/form-data request, Werkzeug separates standard text form fields from binary file streams. Each part undergoes validation against specific memory and disk thresholds.

Werkzeug enforces an internal memory threshold: max_form_memory_size defaults to 500KB. Non-file text form parts are stored in RAM up to this ceiling. If an individual text field exceeds 500KB, Werkzeug halts parsing and raises a RequestEntityTooLarge error. This mechanism shields applications from memory exhaustion attacks triggered by oversized text fields or deep JSON strings submitted via multipart forms.

File uploads, by contrast, behave differently once they pass the initial Content-Length check. Werkzeug relies on Python's tempfile.SpooledTemporaryFile to manage binary file streams:

  1. In-Memory Buffer (< 500KB): When a file upload begins, Werkzeug stores incoming bytes in a memory buffer. Files smaller than 500KB remain entirely in RAM for fast processing.
  2. Disk Spooling (>= 500KB): Once an uploaded file stream crosses the 500KB threshold, Python's spooled temporary file rolls over from RAM to an unlinked physical temporary file on disk (typically within /tmp on Linux systems).
  3. FileStorage Abstraction: Your route receives a FileStorage object wrapping this temporary file. You can read, seek, or save the stream using file.save(destination_path).

Because Werkzeug spools files exceeding 500KB to disk, an application accepting 100 MB uploads will not consume 100 MB of system RAM per concurrent request. However, it will consume temporary disk space until the request completes.

How MAX_CONTENT_LENGTH Differs from max_form_memory_size

Developers frequently confuse MAX_CONTENT_LENGTH with max_form_memory_size. While both directives trigger an HTTP 413 error when breached, they govern completely different aspects of request processing:

Directive Scope Default Value Target Component
MAX_CONTENT_LENGTH Total HTTP request body None (unlimited) Entire request body across all parts and files
max_form_memory_size Individual non-file form fields 500KB (500,000 bytes) Text fields inside multipart/form-data requests
MAX_FORM_PARTS Maximum form parts allowed 1,000 parts Total number of form fields and files in a request

If a user uploads a 10 MB file and your application sets MAX_CONTENT_LENGTH = 16 * 1024 * 1024, the upload succeeds. Werkzeug buffers the first 500KB in memory, spools the remaining 9.5 MB to a temporary file on disk, and passes the resulting FileStorage object to your view.

Conversely, if a client sends a multipart form containing a single text field with 2 MB of raw data, Werkzeug rejects the request due to max_form_memory_size, even if MAX_CONTENT_LENGTH is set to 50 MB. If your application legitimately requires massive text fields, raise the limit with the MAX_FORM_MEMORY_SIZE config key (Flask 3.1 and later) or customize max_form_memory_size on a custom request class:

from flask import Flask, Request

class CustomRequest(Request):
    max_form_memory_size = 2 * 1024 * 1024  # 2 MB limit for non-file text fields

app = Flask(__name__)
app.request_class = CustomRequest
app.config['MAX_CONTENT_LENGTH'] = 32 * 1024 * 1024  # 32 MB total limit

Disk IO Overhead and Container Storage Constraints

While disk spooling protects server RAM, it introduces secondary performance constraints that appear under production workloads.

Every file upload exceeding 500KB requires write operations to the host filesystem followed by read operations when saving the file to its final destination. In high-throughput APIs handling concurrent uploads, continuous disk I/O can degrade performance and saturate storage controllers.

In containerized environments such as Kubernetes pods or AWS ECS tasks, container storage often uses ephemeral disk overlays with strict size quotas. Ten concurrent uploads of 200 MB each require 2 GB of available space in /tmp. If the temporary filesystem runs out of space, Python raises an OSError with code Errno 28 indicating that no space is left on the device, causing unhandled 500 Internal Server Errors rather than controlled 413 rejections.

Werkzeug unlinks and destroys temporary files created by SpooledTemporaryFile when the FileStorage stream closes at the end of the request context. However, if your code spawns background threads that reference the file after the request finishes, the temporary file may be closed prematurely, resulting in an I/O operation error on a closed file.

Why Nginx Rejects Uploads Before Flask Sees Them

A frequent point of confusion in Python web deployments occurs when an engineer configures app.config['MAX_CONTENT_LENGTH'] = 50 * 1024 * 1024, deploys the application behind Nginx and Gunicorn, and tests a 15 MB upload. Instead of receiving the custom JSON error response defined in their Flask error handler, the client receives a generic, unstyled HTML page stating 413 Request Entity Too Large.

This failure occurs because Nginx sits in front of Gunicorn and evaluates request limits before forwarding data upstream.

Nginx enforces its own request body limit through the client_max_body_size directive. By default, Nginx sets client_max_body_size to 1m (1 megabyte).

When a client initiates an HTTP POST request with a 15 MB payload, it sends a Content-Length: 15728640 header. Nginx reads the request line and headers. Comparing the declared size against its default 1 MB limit, Nginx terminates the connection immediately. It generates an internal HTTP 413 response and closes the TCP socket. The request never reaches the Gunicorn WSGI server, and your Flask application code never executes.

To allow larger uploads through Nginx to Flask, you must align client_max_body_size in your Nginx configuration with your Flask MAX_CONTENT_LENGTH setting:

http {
    client_max_body_size 10M;
    server {
        listen 80;
        server_name api.example.com;
        location / {
            proxy_pass http://127.0.0.1:8000;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        }
        location /api/v1/uploads/ {
            client_max_body_size 50M;
            proxy_pass http://127.0.0.1:8000;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_read_timeout 300s;
            proxy_connect_timeout 75s;
        }
    }
}

After modifying /etc/nginx/nginx.conf, test the syntax with nginx -t and reload the service using nginx -s reload.

How to Diagnose the Originating Rejection Layer

When troubleshooting an unexpected 413 error in staging or production, identifying which component rejected the request determines the necessary fix:

  • Nginx Rejection: The response contains a Server: nginx header, the body consists of default Nginx HTML, and your Gunicorn application access logs show no entry for the request. The solution is increasing client_max_body_size in nginx.conf.
  • Flask / Werkzeug Rejection: The response matches your application's custom JSON schema or default Werkzeug HTML, and Gunicorn logs record an incoming request returning status code 413. The solution is adjusting MAX_CONTENT_LENGTH in app.config.
  • Cloudflare Edge Rejection: If your domain routes through Cloudflare's proxy, Cloudflare inspects payload sizes at the edge network. Free and Pro accounts enforce a strict 100 MB request body limit, Business accounts allow 200 MB, and Enterprise plans support up to 5 GB. If an upload exceeds your plan threshold, Cloudflare returns an edge 413 error before Nginx or Flask are ever contacted.

Comparing Request Body Limits Across Web Layers

To prevent unexpected payload rejections, every layer along the request path must be configured to accommodate your target upload size. The lowest limit along the chain dictates the effective ceiling:

Infrastructure Layer Default Payload Limit Configuration Setting Failure Mechanism
Nginx Reverse Proxy 1 MB (1m) client_max_body_size Returns Nginx 413 HTML; closes client TCP socket
Apache HTTP Server 1 GiB (or unlimited) LimitRequestBody Returns Apache 413 error page
Microsoft IIS 30 MB (30,000,000 bytes) maxAllowedContentLength Returns IIS 413.1 or 404.13 status code
Cloudflare Proxy (Free/Pro) 100 MB Cloudflare zone network settings Edge 413 rejection; origin is never contacted
Cloudflare Proxy (Business) 200 MB Cloudflare zone network settings Edge 413 rejection; origin is never contacted
Gunicorn WSGI Server Unlimited Inherits worker request processing Forwards body to Flask application layer
Flask Application None (unlimited) app.config['MAX_CONTENT_LENGTH'] Raises RequestEntityTooLarge (HTTP 413)
Werkzeug Form Parser 500KB (memory threshold) Request.max_form_memory_size Spools files to disk; rejects text fields > 500KB

When configuring an application to handle 30 MB uploads, you must set Nginx client_max_body_size 35M; (providing headroom for multipart boundary overhead) and Flask app.config['MAX_CONTENT_LENGTH'] = 35 * 1024 * 1024.

Fastio features

Resolve Flask MAX_CONTENT_LENGTH Bottlenecks with Intelligent Workspaces

Connect autonomous agents and applications to persistent Fastio workspaces over MCP. Store multi-gigabyte files, index documents for hybrid search, and avoid Flask MAX_CONTENT_LENGTH upload errors. Monthly plans start with a 14-day free trial.

When to Use Streaming and Chunked Uploads in Flask

Raising MAX_CONTENT_LENGTH and client_max_body_size to hundreds of megabytes or gigabytes solves the immediate 413 rejection, but it introduces architectural bottlenecks in production web applications.

Standard WSGI application servers like Gunicorn operate with a fixed pool of synchronous worker processes (typically 2 to 4 workers per CPU core). When a client uploads a 500 MB file over an average consumer connection, that upload may take 30 to 60 seconds to complete. During this entire transfer window, a Gunicorn worker remains tied to the incoming socket, reading data into temporary storage.

If four users initiate large file transfers simultaneously, a four-worker Gunicorn service becomes completely saturated. All incoming requests for login pages, API queries, and lightweight assets must wait in the socket backlog, leading to gateway timeouts (HTTP 504) across your entire platform.

Overcoming this limitation requires moving away from monolithic multipart uploads toward streaming, chunking, or direct-to-storage architectures.

How to Consume Raw Request Streams in Flask

For endpoints that ingest raw binary files, disk images, or streaming sensors, you can bypass Werkzeug's multipart parser entirely by reading from request.stream.

Reading directly from request.stream processes incoming bytes iteratively in fixed chunk sizes (such as 64KB or 1 MB). The application writes each chunk directly to an external destination, permanent storage path, or hashing engine without buffering the full file into memory:

import os
from flask import Flask, jsonify, request
from werkzeug.exceptions import RequestEntityTooLarge

app = Flask(__name__)
app.config['MAX_CONTENT_LENGTH'] = 500 * 1024 * 1024  # 500 MB limit

@app.route('/stream-upload', methods=['POST'])
def stream_upload():
    destination_file = '/var/data/uploads/received_asset.bin'
    chunk_size = 64 * 1024  # 64 KB read buffer
    total_bytes = 0
    with open(destination_file, 'wb') as f:
        while True:
            chunk = request.stream.read(chunk_size)
            if not chunk:
                break
            f.write(chunk)
            total_bytes += len(chunk)
    return jsonify({
        "status": "success",
        "bytes_stored": total_bytes
    }), 201

When reading request.stream, do not access request.form, request.files, or request.data earlier in the request lifecycle. Accessing those properties triggers Werkzeug's multipart parser, consuming the stream before your manual read loop begins.

How Client-Side Chunking Prevents Server Timeouts

For web interfaces and mobile applications where users upload large media assets, client-side chunking provides a resilient alternative to single-socket uploads.

In a chunked architecture, the frontend client splits a large file into smaller slices (typically 5 MB to 10 MB each) using the browser Blob.slice() API. Each slice is transmitted as an independent HTTP POST or PUT request:

  1. Bypasses Proxy Ceilings: Because every chunk is under 10 MB, transfers never approach the 100 MB Cloudflare ceiling or standard Nginx buffer limits.
  2. Resilience to Network Drops: If a connection fails midway through a large file transfer, the client only retries the failed 10 MB chunk rather than restarting the entire upload from byte zero.
  3. Reduced Worker Occupation: Each request finishes in fractions of a second, freeing Gunicorn workers to handle other traffic.

Standardized protocols like Tus or custom chunked handlers store parts on disk or object storage, assembling the complete asset once the final chunk arrives.

How Presigned Direct-to-Storage Uploads Eliminate Web Server Load

The cleanest architectural pattern for high-scale applications completely removes file payload bytes from the Flask application server.

Instead of routing multi-megabyte payloads through Nginx, Gunicorn, and Flask:

  1. The client requests an upload authorization token from a lightweight Flask API endpoint (POST /api/upload-ticket).
  2. Flask validates user authentication and permissions, determines target storage keys, and generates a presigned URL with an expiration window.
  3. The client uploads the binary payload directly to the storage service via HTTP PUT.
  4. Upon transfer completion, the storage service fires an event, or the client notifies Flask (POST /api/upload-complete) to record file metadata in the application database.

This pattern isolates your Flask API from network bandwidth bottlenecks, eliminates 413 configuration issues across intermediate proxies, and allows your application servers to remain lightweight and stateless.

How AI Agent Workspaces Manage Large Corpora Beyond Upload Limits

In modern engineering workflows, Flask microservices frequently serve as ingestion gateways for artificial intelligence agents, autonomous coding assistants, and context retrieval pipelines. Developers build upload endpoints to ingest legal binders, technical documentation, financial reports, and codebase archives for downstream language model analysis.

However, routing large document collections through standard HTTP APIs quickly hits both network boundaries and model inference constraints.

Context limits enforce strict boundaries in AI environments. As documented by the Anthropic Help Center, Claude chat sessions accept up to 20 files at up to 500 MB per file, while Claude Projects accept files up to 30 MB each with no fixed file-count cap, bounded only by the model's context window.

When development teams or autonomous agent workflows attempt to attach massive document sets directly to language model prompts, they encounter context window exhaustion. Similarly, pushing large corporate archives through Flask REST endpoints to feed local vector databases triggers Gunicorn memory pressure, proxy timeouts, and HTTP 413 rejections.

Solving this coordination problem requires decoupling file storage and search indexing from the application server.

How to Decouple File Storage from Agent Context via MCP

Rather than streaming entire file collections through Flask endpoints or attaching raw files to LLM prompts, teams store corpora in persistent, searchable cloud workspaces.

Fastio provides intelligent workspaces designed for human teams and autonomous agents. When documents land in a Fastio workspace, Intelligence Mode automatically indexes the contents for hybrid search, combining full-text indexing, semantic search, and metadata filtering. Instead of loading an entire 500 MB archive into an LLM context window, agents search the workspace index and retrieve only the specific passages and facts needed to answer a query, complete with source document citations.

Autonomous agents and developer tools connect to workspaces over Streamable HTTP using the remote Model Context Protocol (MCP) server. Teams building storage for agents can connect assistants at https://mcp.fast.io/mcp/tools (for general assistants and Claude applications) or https://mcp.fast.io/mcp/code (for coding agents like Claude Code, Cursor, and Gemini CLI). Full setup instructions are documented at Fastio MCP Documentation.

Through MCP, an agent queries documents across the organization without requiring local disk allocation, file streaming across Flask proxies, or context-saturating attachments.

How Cloud Sync and Intelligent Workspaces Replace Custom Upload Forms

Ingesting corporate data into an agent workspace does not require writing custom Flask upload forms or managing complex chunking scripts.

Fastio Cloud Sync connects Google Drive, Dropbox, Box, and OneDrive folders directly to a workspace. Synchronization runs one-way or two-way, on a schedule or on demand. When team members update files in an external Google Drive or OneDrive folder, the workspace syncs the updates, triggers background indexing, and makes the updated knowledge available to connected agents.

For applications that need direct programmatic uploads, Fastio handles large file transfers through chunked upload mechanics, supporting large assets up to plan limits: 25 GB on Starter, 50 GB on Business, and 100 GB on Enterprise.

Monthly plans start with a 14-day free trial that requires a credit card. Plans detailed on the workspace pricing page include Starter at $9.99/mo (3 seats, 250 GB storage, 5 workspaces, 100,000 credits a month), Business at $49.99/mo (10 seats, 5 TB storage, 50 workspaces, 600,000 credits a month), and Enterprise at $199.99/mo (30 seats included, 25 TB storage, 200 workspaces, 3,000,000 credits a month).

By delegating file persistence, synchronization, and RAG indexing to an intelligent workspace, your Flask microservices stay lightweight and avoid most HTTP 413 upload bottlenecks.

Sources

References used to verify factual claims in this guide.

  1. Flask raises an HTTP 413 RequestEntityTooLarge error when an incoming request exceeds the byte limit configured by MAX_CONTENT_LENGTH.

  2. Claude chat sessions accept up to 20 files at up to 500 MB per file, while Claude Projects accept files up to 30 MB each with no fixed file-count cap, bounded only by the model's context window.

Frequently Asked Questions

How do I set the maximum upload file size in Flask?

Set the `MAX_CONTENT_LENGTH` configuration key on your Flask application instance to the desired byte limit, such as `app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024` for a 16 MB ceiling. If an incoming request body exceeds this number of bytes, Flask halts processing and raises a `RequestEntityTooLarge` (HTTP 413) exception.

Why do I get 413 Request Entity Too Large in Flask?

An HTTP 413 error occurs when an incoming request body exceeds the configured `MAX_CONTENT_LENGTH` in Flask, or exceeds the `client_max_body_size` directive on an intermediate reverse proxy like Nginx. Flask checks the `Content-Length` header before reading data and aborts the request if the declared size is larger than the allowable threshold.

How does Werkzeug handle file uploads larger than MAX_CONTENT_LENGTH?

Werkzeug checks the `Content-Length` header against `MAX_CONTENT_LENGTH` during initial request parsing. If the header exceeds the limit, Werkzeug immediately raises `werkzeug.exceptions.RequestEntityTooLarge` without reading the file stream. If chunked transfer encoding is used without a content length, Werkzeug reads stream chunks until the accumulated byte count crosses the limit, at which point it halts and raises 413.

Why does Nginx return a 413 error even after setting MAX_CONTENT_LENGTH in Flask?

Nginx enforces its own payload boundary via `client_max_body_size`, which defaults to `1m` (1 megabyte). Because Nginx sits in front of Gunicorn or Flask, it evaluates the `Content-Length` header first. If an upload exceeds 1 MB, Nginx terminates the connection and returns its own HTML 413 error before Gunicorn or Flask ever receives the request.

How can Flask stream request data without loading entire files into memory?

You can stream incoming request bodies iteratively by reading directly from `request.stream` using a loop with a fixed buffer size, such as `chunk = request.stream.read(64 * 1024)`. Avoid accessing `request.files`, `request.form`, or `request.data`, as reading those properties triggers Werkzeug's multipart parser and consumes the stream into temporary storage.

How do AI agents access large file collections that exceed context windows and HTTP upload limits?

Rather than streaming multi-gigabyte document collections through Flask endpoints or attaching large files directly to model prompts, teams place files into intelligent cloud workspaces like Fastio. The workspace indexes the corpus for hybrid search, and agents connect via the Model Context Protocol (MCP) server to query specific passages without exceeding HTTP upload limits or context windows.

Related Resources

Fastio features

Resolve Flask MAX_CONTENT_LENGTH Bottlenecks with Intelligent Workspaces

Connect autonomous agents and applications to persistent Fastio workspaces over MCP. Store multi-gigabyte files, index documents for hybrid search, and avoid Flask MAX_CONTENT_LENGTH upload errors. Monthly plans start with a 14-day free trial.