python 116 lines · 3 tabs

Streaming Large Multipart File Uploads to Disk in Flask Without Buffering

Shared by codesnips Aug 2026
3 tabs
import os
import uuid

from flask import Blueprint, current_app, jsonify, request
from werkzeug.exceptions import RequestEntityTooLarge

from .streaming import make_stream_factory, DiskStreamTarget
from .safe_names import SafeNames

bp = Blueprint("uploads", __name__)

MAX_FILE_BYTES = 5 * 1024 * 1024 * 1024  # 5 GiB per part


@bp.route("/uploads", methods=["POST"])
def stream_upload():
    root = current_app.config["UPLOAD_ROOT"]
    names = SafeNames(root)
    upload_id = uuid.uuid4().hex

    written = []
    factory = make_stream_factory(names, upload_id, MAX_FILE_BYTES, written)

    try:
        # Cap the whole body; parse_form_data streams each part via the factory.
        request.max_content_length = MAX_FILE_BYTES * 4
        _, files = request.parse_form_data(
            request.environ, stream_factory=factory
        )
    except RequestEntityTooLarge:
        _cleanup(written)
        return jsonify(error="file too large"), 413
    except Exception:
        _cleanup(written)
        raise

    saved = [
        {"field": key, "path": f.stream.path, "bytes": f.stream.bytes_written}
        for key, f in files.items(multi=True)
        if isinstance(f.stream, DiskStreamTarget)
    ]
    return jsonify(upload_id=upload_id, files=saved), 201


def _cleanup(paths):
    for path in paths:
        try:
            os.unlink(path)
        except OSError:
            pass
3 files · python Explain with highlit

By default, Flask (via Werkzeug) buffers an incoming multipart/form-data body: small files land in memory and larger ones spill into a temporary file that is then re-read by application code. For big uploads this doubles disk I/O and can pin large amounts of memory. This snippet shows how to intercept the parse so each field is streamed straight to its final destination, giving constant memory use regardless of upload size.

The stream_upload endpoint disables Werkzeug's automatic form parsing by setting request.max_content_length for a hard cap and calling request.parse_form_data with a custom stream_factory. Werkzeug invokes that factory once per file part and hands back a writable stream that it feeds in chunks as the socket delivers them. The factory returned by make_stream_factory opens the target path with os.open using O_CREAT | O_EXCL so a duplicate upload_id fails loudly instead of clobbering data, then wraps the fd so the caller can .write() directly to disk.

DiskStreamTarget is the thin wrapper Werkzeug writes into. It tracks bytes_written and enforces max_bytes inline, raising RequestEntityTooLarge mid-stream the moment a part exceeds the limit — important because relying on Content-Length alone lets a lying client exhaust disk. Because the wrapper implements write, seek, tell, and close, Werkzeug treats it like a normal file object and never allocates a SpooledTemporaryFile.

The SafeNames helper centralizes path safety: secure_join runs the client filename through secure_filename and resolves the result under a fixed root, rejecting any path that escapes it. This defends against directory-traversal payloads like ../../etc/passwd that arrive in the filename header of a part.

The key trade-off is that a streamed part is committed to disk as it arrives, so a validation failure after the fact requires an explicit cleanup — the endpoint deletes partial files in an except block. This pattern suits ingestion services, media pipelines, or any endpoint accepting multi-gigabyte uploads where buffering the whole body is not viable. For small forms the default parser is simpler and preferable.


Related snips

Share this code

Here's the card — post it anywhere.

Streaming Large Multipart File Uploads to Disk in Flask Without Buffering — share card
Link copied