FastAPI Upload File with Multipart Handling and Streaming to Storage

Posted on
FastAPI Upload File with Multipart Handling and Streaming to Storage

A simple FastAPI endpoint using file: bytes works fine for small demos. But try uploading a 2 GB video, and things can go wrong quickly. If your container only has 512 MB of memory, loading the whole file into memory can cause it to run out of space and crash.

You may not even get a useful error. Sometimes, all you see is a memory-related message in the logs followed by a container restart.

Most FastAPI upload file tutorials work fine until a large file hits a small container. FastAPI gives you a few ways to handle uploaded files, and each one uses memory differently.

FastAPI uses UploadFile to handle uploaded files without keeping the whole file in memory. Multipart data can be stored in a temporary file, which is much safer for large uploads.

For large files, a good production setup is to validate the file early, stream it directly to object storage, and avoid loading the entire file into memory.

Another option is to use a managed upload service like Filestack. This keeps the actual file transfer out of your API server, so your FastAPI app doesn’t have to handle the file bytes itself.

This article covers each approach, starting with the simple buffered setup that can struggle under heavy uploads and ending with a setup that keeps the file transfer completely off your servers.

Key Takeaways

  • A bytes parameter loads the entire upload into RAM. UploadFile is the safer default for anything sizable.
  • UploadFile wraps SpooledTemporaryFile: small files stay in memory, large ones spill to disk on their own.
  • request.stream() yields chunks with no temp file at all, which keeps memory flat while you proxy to storage.
  • Content-Length checks and part-size limits belong before you read the body, not after.
  • A managed file upload api can take bytes out of your service entirely, leaving your endpoints to handle auth and metadata only.

UploadFile Under the Hood

FastAPI gives you two ways to accept a file, and they behave very differently under load.

Declare the parameter as bytes, and FastAPI reads the whole upload into memory before your function even runs. This works for small files and fails without warning as soon as someone uploads something large. UploadFile is the answer most FastAPI docs point to for a general file upload api, and for good reason: it wraps Starlette’s SpooledTemporaryFile, which keeps small files in memory and spills larger ones to disk automatically, past a configurable threshold. Your endpoint code stays the same either way. Only where the bytes live changes.

from fastapi import FastAPI, UploadFile, File

app = FastAPI()

@app.post("/upload")

async def upload_file(file: UploadFile = File(...)):

contents = await file.read()

# process contents, or better, stream it in chunks below

await file.close()

return {"filename": file.filename, "size": len(contents)}

This basic form is fine for small files, and it’s already safer than a bytes parameter since Starlette manages the spool for you. It still reads the full file into a variable at once, which is where the next section picks up.

Multipart Done Right

A multipart request isn’t a single file. It’s a series of parts separated by a boundary string, and each part can hold a form field or a file, mixed in any order.

Understanding how does multipart upload work in web applications helps explain why FastAPI needs a parser at all: the framework has to read the boundary, split the body into parts, and hand each one to the right parameter based on its name.

FastAPI handles this parsing for you with UploadFile and Form, so most endpoints don’t need to work with the raw multipart request.

If you’re asking how to add file uploads to a REST API, the basic approach is usually the same: accept the file as multipart form data, validate it early, and save it somewhere reliable before sending a response.

The main difference between frameworks is how much of this work they handle for you.

Set limits on file size and the number of parts before processing the upload. An unlimited number of file parts can use up memory or disk space, even when each file is small.

Multipart parsing gets the file into your endpoint. The next challenge is sending it somewhere else without using too much memory. That’s what we’ll cover next.

Filestack discord

Streaming Straight to Storage

Reading the entire file into a variable still means your app has to hold the full file while processing it. For large uploads, that’s not ideal.

A better approach is to stream the file directly to storage in small chunks. With request.stream(), you can read the request body piece by piece without creating a temporary file.

If you’re uploading to your own S3 bucket, the flow is simple: read one chunk, send it as an S3 multipart upload part, then move to the next chunk. You only keep one chunk in memory at a time, so memory usage stays much more predictable even as file sizes grow.

import boto3

from fastapi import FastAPI, Request

app = FastAPI()

s3 = boto3.client("s3")

BUCKET = "your-upload-bucket"

CHUNK_SIZE = 5 * 1024 * 1024  # 5MB, S3's minimum part size

@app.post("/upload-stream/{key}")

async def upload_stream(key: str, request: Request):

upload = s3.create_multipart_upload(Bucket=BUCKET, Key=key)

upload_id = upload["UploadId"]

parts = []

part_number = 1

buffer = b""

try:

async for chunk in request.stream():

buffer += chunk

while len(buffer) >= CHUNK_SIZE:

part_data, buffer = buffer[:CHUNK_SIZE], buffer[CHUNK_SIZE:]

result = s3.upload_part(

Bucket=BUCKET, Key=key, UploadId=upload_id,

PartNumber=part_number, Body=part_data,

)

parts.append({"PartNumber": part_number, "ETag": result["ETag"]})

part_number += 1

if buffer:

result = s3.upload_part(

Bucket=BUCKET, Key=key, UploadId=upload_id,

PartNumber=part_number, Body=buffer,

)

parts.append({"PartNumber": part_number, "ETag": result["ETag"]})

s3.complete_multipart_upload(

Bucket=BUCKET, Key=key, UploadId=upload_id,

MultipartUpload={"Parts": parts},

)

except Exception:

s3.abort_multipart_upload(Bucket=BUCKET, Key=key, UploadId=upload_id)

raise

return {"key": key, "parts": len(parts)}

This approach adds a little more code, but it gives you an important benefit: memory usage stays close to CHUNK_SIZE, no matter how large the file is.

That only works if you validate the incoming request first. So the next step is making sure those checks are in place.

Limits, Validation and Errors

Streaming protects your memory while the upload is running, but it doesn’t stop a bad request from starting. You should validate the request before reading any file data.

First, check Content-Length against your maximum file size. If the header is missing or can’t be trusted, keep checking the size as you read each chunk and stop as soon as the limit is reached.

You should also check the actual file type. Don’t rely only on the file extension or the MIME type sent by the client because those can be wrong.

Check When Response
Content-Length vs max size Before reading body 413 Payload Too Large
Multipart part count During parsing 400 Bad Request
Content-type mismatch After first chunk read 415 Unsupported Media Type
Chunk count exceeds limit while streaming During stream loop Abort upload, 413

These same limits apply to any REST API that handles file uploads, no matter which framework you use. FastAPI simply gives you the tools to check them early.

Once your own upload endpoint is properly limited and validated, there’s another option: don’t handle the file upload yourself at all. That’s the next approach.

The Managed Route, Off the Data Path

The final option is to keep the actual file upload out of FastAPI completely. Instead, clients upload directly through a managed file upload API, while your FastAPI endpoints handle things like authentication and file metadata.

Your server can provide short-lived upload credentials; the client sends the file directly to storage, and your API saves the file details once the upload is complete.

The main benefit is that the file data never passes through your FastAPI container, which keeps your server lighter and reduces memory and bandwidth pressure.

Diagram showing FastAPI upload file flow streaming multipart data straight to storage.

If you don’t want to build and maintain your own file upload infrastructure, a managed service can handle that part for you.

Filestack’s REST API supports file uploads up to 5 GB and uses chunked uploads for large files. This lets your FastAPI service focus on things like authentication and file metadata instead of moving the actual file data.

import requests

FILESTACK_API_KEY = "YOUR_API_KEY"

def get_upload_url(filename: str) -> dict:

response = requests.post(

f"<https://www.filestackapi.com/api/store/S3?key={FILESTACK_API_KEY}>",

params={"filename": filename},

)

response.raise_for_status()

return response.json()  # contains the URL the client uploads to directly

Your endpoint only needs to provide the upload URL and save the returned file handle after the upload succeeds. The actual file transfer happens outside your service.

Now that we’ve covered all four approaches, here’s a quick summary of what to do.

Conclusion: Choose Your Memory Tier on Purpose

FastAPI gives you four main ways to handle file uploads. The key difference is where the file data goes while it’s being uploaded.

Buffer keeps the whole file in memory. Spool, which is what UploadFile uses, keeps smaller files in memory and moves larger ones to disk. Stream sends the file in chunks directly to storage, keeping memory usage low. Bypass skips your server completely and lets the client upload directly to storage.

Choose the approach based on your file sizes and infrastructure instead of waiting for a large upload to crash your container.

A good first step is to replace any bytes parameter that is still being used for real file uploads.

💡The guide to handling large file uploads covers the client side of this same problem, and the Filestack API docs walk through the direct-to-storage flow in full.

Frequently Asked Questions

Does FastAPI load uploads into memory?

A bytes parameter does, in full. UploadFile spools to a temporary file once the upload passes a size threshold, so small files stay in memory, and large ones move to disk automatically.

How do I stream an upload to S3 from FastAPI?

Iterate over request.stream() and write each chunk into an S3 multipart upload. Memory use stays constant regardless of file size, since you only hold one chunk at a time.

What is the maximum upload size in FastAPI?

FastAPI itself sets no cap. Your reverse proxy, server configuration, and any validation you add are what actually set the limit.

Read More →