python 149 lines · 3 tabs

Streaming FastAPI File Uploads with a Background Validation Task

Shared by codesnips Jul 2026
3 tabs
import enum
import datetime as dt

from sqlalchemy import Column, Integer, String, BigInteger, Enum, DateTime
from sqlalchemy.orm import declarative_base

Base = declarative_base()


class UploadStatus(str, enum.Enum):
    pending = "pending"
    valid = "valid"
    invalid = "invalid"
    errored = "errored"


class Upload(Base):
    __tablename__ = "uploads"

    id = Column(Integer, primary_key=True)
    original_name = Column(String(255), nullable=False)
    stored_path = Column(String(512), nullable=False, unique=True)
    content_type = Column(String(127), nullable=True)
    size = Column(BigInteger, nullable=False, default=0)
    sha256 = Column(String(64), nullable=True)
    status = Column(Enum(UploadStatus), nullable=False, default=UploadStatus.pending)
    reason = Column(String(512), nullable=True)
    created_at = Column(DateTime, default=dt.datetime.utcnow, nullable=False)

    def public_dict(self):
        return {
            "id": self.id,
            "original_name": self.original_name,
            "status": self.status.value,
            "size": self.size,
            "reason": self.reason,
        }
3 files · python Explain with highlit

This snippet shows how FastAPI accepts a file upload, streams it to disk without exhausting memory, records a pending row, and then hands off content validation to a BackgroundTask so the request returns quickly.

The route in uploads router uses UploadFile = File(...), which gives a SpooledTemporaryFile under the hood: small files stay in memory, larger ones spill to disk. Rather than calling await file.read() (which pulls the entire body into RAM), the handler reads in fixed chunks with await file.read(CHUNK_SIZE) and writes them out, so a 500MB upload costs a few KB of working memory. A SHA-256 hash is accumulated during the same pass, giving a content fingerprint for free. The saved path uses a random uuid4 name to avoid collisions and path-traversal from user-controlled filenames.

Before returning, the handler inserts an Upload row in state pending and schedules validate_upload via background_tasks.add_task(...). FastAPI runs that callable after the response is sent, so the client gets an immediate 202 Accepted while heavier work continues out of band. This is the key trade-off: validation is eventually-consistent, so clients must poll or subscribe for the final status instead of expecting it inline.

validation task opens a fresh database session — background tasks run outside the request scope, so the request-bound session is already closed. It re-reads the file to check the real content type with python-magic, enforces a size ceiling, and flips the row to valid or invalid, storing a reason. Wrapping the body in try/except and marking a row errored matters because an unhandled exception in a background task is logged but invisible to the user.

The Upload model defines the durable state machine via an UploadStatus enum and stores sha256, size, and content_type. Because validation is idempotent on the stored path, a failed task can be safely retried. For truly durable processing across restarts a real queue like Celery or RQ is preferable, but BackgroundTask is the right lightweight tool when the work is short and best-effort.


Related snips

Share this code

Here's the card — post it anywhere.

Streaming FastAPI File Uploads with a Background Validation Task — share card
Link copied