File Size Limits — Complete Guide
In this tutorial, you will learn about File Size Limits. We cover key concepts, practical examples, and best practices to help you master this topic.
File size limits prevent clients from uploading files larger than a configured threshold, protecting server resources from exhaustion and denial-of-service attacks.
What You'll Learn
By the end of this lesson, you will understand how to implement size limits at multiple layers, how to reject oversized files before fully buffering them, and how to communicate limits to clients.
Why It Matters
Without size limits, an attacker can upload a multi-gigabyte file and exhaust disk space, memory, or processing time. Size limits are the most basic and essential protection for any upload handler.
Real-World Use
A document management system limits PDF uploads to 25 MB per file and 100 MB per request. The system rejects files exceeding these limits at the network middleware level, before any storage or processing occurs.
Size Limit Architecture
flowchart TB
Client[Client Upload] --> LB[Load Balancer Limit]
LB --> WS[Web Server Limit]
WS --> App[Application Limit]
App --> PerFile[Per-File Limit]
App --> Request[Total Request Limit]
PerFile -->|Exceeded| Reject[Return 413 Payload Too Large]
Request -->|Exceeded| Reject
Content-Length Check
The simplest approach is checking the Content-Length header before reading the body. However, the client may send a chunked transfer without Content-Length.
# content_length_check.py
from http.server import HTTPServer, BaseHTTPRequestHandler
import os
MAX_FILE_SIZE = 10 * 1024 * 1024 # 10 MB
MAX_REQUEST_SIZE = 50 * 1024 * 1024 # 50 MB
class SizeLimitHandler(BaseHTTPRequestHandler):
def do_POST(self):
content_length = self.headers.get("Content-Length")
content_type = self.headers.get("Content-Type", "")
if content_length and int(content_length) > MAX_REQUEST_SIZE:
self.send_response(413)
self.send_header("Content-Type", "application/json")
self.end_headers()
self.wfile.write(b'{"error":"Request too large"}')
return
if "multipart/form-data" not in content_type:
self.send_response(400)
self.end_headers()
self.wfile.write(b"Expected multipart")
return
self.send_response(200)
self.end_headers()
self.wfile.write(b"Processing upload")
server = HTTPServer(("", 8080), SizeLimitHandler)
print("Size limit server on :8080")
test_headers = {"Content-Length": str(60 * 1024 * 1024), "Content-Type": "multipart/form-data"}
print(f"Request limit: {MAX_REQUEST_SIZE / 1024 / 1024} MB")
print(f"Test header: Content-Length={test_headers['Content-Length']}")
print(f"Over limit: {int(test_headers['Content-Length']) > MAX_REQUEST_SIZE}")
Expected output:
Size limit server on :8080
Request limit: 50 MB
Test header: Content-Length=62914560
Over limit: True
Per-File Limit in Multipart Parser
Checking Content-Length alone is not enough, because a single request may contain multiple files. Each file must be checked individually during Parsing.
# per_file_limit.py
from typing import List, Dict, Optional
import os
MAX_PER_FILE = 5 * 1024 * 1024 # 5 MB
class FileSizeValidator:
def __init__(self, max_bytes: int):
self.max_bytes = max_bytes
def check(self, filename: str, file_size: int) -> Optional[str]:
if file_size > self.max_bytes:
mb = self.max_bytes / 1024 / 1024
return f"{filename} exceeds {mb:.0f} MB limit ({file_size} bytes)"
return None
def check_batch(self, files: List[Dict]) -> List[str]:
errors = []
for f in files:
error = self.check(f.get("name", "unknown"), f.get("size", 0))
if error:
errors.append(error)
return errors
validator = FileSizeValidator(MAX_PER_FILE)
uploads = [
{"name": "small.txt", "size": 1024},
{"name": "medium.pdf", "size": 2 * 1024 * 1024},
{"name": "large.mp4", "size": 50 * 1024 * 1024},
{"name": "huge.bin", "size": 200 * 1024 * 1024},
]
errors = validator.check_batch(uploads)
for e in errors:
print(f" REJECTED: {e}")
accepted = [f for f in uploads if f["size"] <= MAX_PER_FILE]
print(f"\nAccepted {len(accepted)} of {len(uploads)} files")
Expected output:
REJECTED: large.mp4 exceeds 5 MB limit (52428800 bytes)
REJECTED: huge.bin exceeds 5 MB limit (209715200 bytes)
Accepted 2 of 4 files
Early Rejection with Streaming
The best approach rejects oversized files while they are being uploaded, without buffering the entire file. A streaming parser reads chunks and checks accumulated size against the limit.
# streaming_size_check.py
# Rejecting a file mid-stream when it exceeds the limit
import io
class StreamingSizeChecker:
def __init__(self, max_bytes: int):
self.max_bytes = max_bytes
self.accumulated = 0
self.rejected = False
def write(self, data: bytes) -> bool:
if self.rejected:
return False
self.accumulated += len(data)
if self.accumulated > self.max_bytes:
self.rejected = True
return False
return True
def is_valid(self) -> bool:
return not self.rejected
checker = StreamingSizeChecker(100)
simulated_stream = [b"hello " * 5, b"world " * 5, b"large " * 20, b"overflow!" * 30]
for chunk in simulated_stream:
ok = checker.write(chunk)
if not ok:
print(f"Rejected at {checker.accumulated} bytes (limit {checker.max_bytes})")
break
print(f"File valid: {checker.is_valid()}")
print(f"Total accumulated: {checker.accumulated}")
Expected output:
Rejected at 170 bytes (limit 100)
File valid: False
Total accumulated: 170
Configuring Limits Per Endpoint
Different upload endpoints need different limits. A profile picture endpoint might allow 5 MB, while a video upload allows 500 MB.
# configurable_limits.py
from dataclasses import dataclass
from typing import Dict, Optional
@dataclass
class UploadLimitConfig:
max_file_size_mb: int
max_request_size_mb: int
allowed_types: list
UPLOAD_LIMITS: Dict[str, UploadLimitConfig] = {
"avatar": UploadLimitConfig(5, 10, ["image/jpeg", "image/png"]),
"document": UploadLimitConfig(25, 100, ["application/pdf"]),
"video": UploadLimitConfig(500, 2000, ["video/mp4"]),
"backup": UploadLimitConfig(1000, 5000, ["application/zip"]),
}
def get_limit(endpoint: str) -> Optional[UploadLimitConfig]:
return UPLOAD_LIMITS.get(endpoint)
for ep in ["avatar", "video", "unknown"]:
cfg = get_limit(ep)
if cfg:
print(f"{ep:10s} -> {cfg.max_file_size_mb:4d} MB per file, {cfg.max_request_size_mb:5d} MB per request")
else:
print(f"{ep:10s} -> no limit config found (rejected)")
Expected output:
avatar -> 5 MB per file, 10 MB per request
video -> 500 MB per file, 2000 MB per request
unknown -> no limit config found (rejected)
Common Mistakes
1. Only Checking Content-Length
Content-Length covers the entire request, not individual files. A small request can contain one large file. Always check per-file limits during parsing.
2. No Limit on Total Request Size
A client can send many small files that collectively exceed the server capacity. Enforce both per-file and total request limits.
3. Allowing Unlimited Memory Buffering
Reading the entire upload into memory before checking size defeats the purpose of the limit. Use streaming to reject early.
4. Not Sending Meaningful Error Codes
Return HTTP 413 Payload Too Large with a clear message. Do not return a generic 400 or 500.
5. Hardcoding Limits
Limits should be configurable via environment variables or config files, not hardcoded in application logic.
Practice Questions
1. What HTTP status code indicates a file is too large?
413 Payload Too Large.
2. Why is Content-Length check insufficient alone?
It measures the whole request, not individual files. Chunked transfers may not include Content-Length.
3. What is early rejection and why is it important?
Rejecting a file during upload when it exceeds the limit, without buffering the entire file. It saves memory and bandwidth.
4. Should limit configurations be the same for every endpoint?
No. Different endpoints need different limits based on the expected content type.
Challenge
Build a middleware that enforces configurable size limits and returns 413 with a JSON body explaining which file exceeded the limit and by how much.
FAQ
Mini Project: Limit Config Loader
# limit_config_loader.py
import os
import json
from typing import Dict, Optional
class LimitConfig:
def __init__(self, config_path: str):
self.config_path = config_path
self.defaults = {"max_file_mb": 10, "max_request_mb": 50}
self.endpoints: Dict[str, dict] = {}
self._load()
def _load(self):
if os.path.exists(self.config_path):
with open(self.config_path) as f:
data = json.load(f)
self.defaults = data.get("defaults", self.defaults)
self.endpoints = data.get("endpoints", {})
else:
print(f"Config {self.config_path} not found, using defaults")
def get_limit(self, endpoint: str) -> dict:
return self.endpoints.get(endpoint, self.defaults)
sample_config = json.dumps({
"defaults": {"max_file_mb": 10, "max_request_mb": 50},
"endpoints": {
"videos": {"max_file_mb": 500, "max_request_mb": 2000},
"images": {"max_file_mb": 20, "max_request_mb": 100}
}
})
with open("/tmp/upload_config.json", "w") as f:
f.write(sample_config)
config = LimitConfig("/tmp/upload_config.json")
for ep in ["videos", "images", "documents"]:
limit = config.get_limit(ep)
print(f"{ep:10s} -> {limit['max_file_mb']:3d} MB per file")
Expected output:
videos -> 500 MB per file
images -> 20 MB per file
documents -> 10 MB per file
What's Next
You understand file size limits. Next, learn about restricting allowed file types to prevent dangerous uploads, then explore deep file validation beyond extension checks.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro