MEDIA_ROOT, MEDIA_URL, FileField/ImageField, and upload handlers
coreintermediateMEDIA_ROOT and MEDIA_URL are user-uploaded content's equivalent of STATIC_ROOT/STATIC_URL — MEDIA_ROOT is the filesystem directory uploads are saved into, MEDIA_URL the prefix they're served under. FileField stores an uploaded file and exposes it as a FieldFile (with .url, .path, .size); ImageField is FileField plus dimension validation and (if Pillow is installed) automatic width/height population, and requires Pillow to even validate as an image field. Upload handlers (UploadedFile subclasses, configured via FILE_UPLOAD_HANDLERS) are the pluggable layer deciding HOW an uploaded file's bytes are received during the request — MemoryFileUploadHandler for small files (kept in memory), TemporaryFileUploadHandler for anything over FILE_UPLOAD_MAX_MEMORY_SIZE (streamed to a temp file on disk instead), chosen automatically based on size.
Think of it as
MEDIA_ROOT/MEDIA_URL mirroring STATIC_ROOT/STATIC_URL is not a coincidence — both solve the same underlying problem (where do these files live on disk, and what URL serves them), just for content with a different origin: static files ship WITH the code, media files are uploaded by users AFTER deployment. That distinction is exactly why Django never serves MEDIA_ROOT in production the way runserver conveniently does in development — user uploads are untrusted, arbitrarily large, and need their own serving/storage strategy (often object storage, not a local directory, once the deployment is not a single small server). Upload handlers exist as a pluggable layer because "how do I receive these bytes" has a real trade-off Django doesn't want to hardcode: buffering a small upload in memory is fast and simple, but doing that for a large file risks exhausting server memory — the automatic switch to a temp-file handler past FILE_UPLOAD_MAX_MEMORY_SIZE is Django choosing the safer strategy once a file crosses a size threshold, without application code needing to think about it. FileField vs ImageField being genuinely different fields (not ImageField as a thin subclass alias) reflects that image-specific concerns — validating it's actually a decodable image, knowing its dimensions — need Pillow and don't apply to an arbitrary uploaded file.
What we're doing: A per-user upload path using a callable upload_to, keeping each user's files in their own directory.
- 1
- upload_to as a callable receives (instance, filename) — instance is available even before the model is saved, since Django knows the model instance being built, just not its final pk yet.
- 7
- The RAW filename argument (line 2) is user-controlled input — see the sibling concept for why it must be validated/sanitized, not trusted directly, even though this example focuses on the path structure.
Why this works: A callable upload_to naturally organizes uploads per-user (or per-date, per-tenant) without any extra bookkeeping — the directory structure alone makes it trivial to locate or bulk-manage one user's files, and avoids dumping every upload into one flat directory.
Assuming reassigning a FileField or deleting its model instance also deletes the old file from storage
Wrong
Better
What you see: Storage usage grows steadily over time with orphaned files that no model instance references anymore — reassigning a FileField or deleting the owning row never triggers an automatic cleanup of the underlying file.
Why: Django deliberately does NOT delete a FileField's underlying file automatically on reassignment or model deletion — the documented reasoning is that a file could be referenced from more than one place, or intentionally kept for other reasons, so automatic deletion could destroy something still needed. Explicit cleanup (a signal receiver, an override of delete(), or a scheduled job) is the documented responsibility of the application, not something the field handles on its own.
- Client → Upload handlers: POST multipart file
- Upload handlers → Upload handlers: size ≤ FILE_UPLOAD_MAX_MEMORY_SIZE → MemoryFileUploadHandler
- Upload handlers → Upload handlers: size > threshold → TemporaryFileUploadHandler (disk)
- Upload handlers → FileField: saved under MEDIA_ROOT via upload_to
Static vs media, side by side
Together
Remember: MEDIA_ROOT/MEDIA_URL mirror STATIC_ROOT/STATIC_URL for user-uploaded content — never served directly by runserver's convenience in production. upload_to (string or callable) controls placement within MEDIA_ROOT. Django never auto-deletes a FileField's old file on reassignment or model deletion — that cleanup is explicit, application-level responsibility. FILE_UPLOAD_MAX_MEMORY_SIZE picks memory-vs-disk handling, not a size cap.
See also: upload validation as untrusted input · object storage and presigned urls · specialized and file fields

