
Dec 23, 2025
Imagine you run a file upload service or API. One day, someone uploads a 50 GB file to your server. Your disk fills up instantly. Your server crashes. All your users can't access the service.
Or worse: A bot makes 1,000 requests, each with a 1 MB payload, consuming all your memory. Your application becomes unresponsive.
Request body size limiting is a critical security feature that protects your server from:
Disk exhaustion: Large files filling up your storage
Memory exhaustion: Large payloads consuming all RAM
Bandwidth waste: Unnecessary data transfer
DoS attacks: Attackers deliberately sending huge requests
Cost: Expensive bandwidth and storage bills
Performance: Server slowdowns from processing large files
Without limits, any client can send unlimited data, and your server must accept it all before processing.
With limits, you control:
Maximum file upload size
Maximum API request size
Maximum body size for form submissions
Different limits for different endpoints
Caddy makes implementing these limits trivial through the request_body directive.
When a client (browser, API client, etc.) sends data to your server, it comes in an HTTP request. The request has:
Headers: Metadata about the request (Content-Type, Authorization, etc.)
Body: The actual data being sent
Examples of request bodies:
| Type | Example | Typical Size |
|---|---|---|
| JSON API request | {"name":"John","age":30} | 1-10 KB |
| Form submission | email=john@example.com&password=... | 1-100 KB |
| Small file upload | Photo from phone | 1-5 MB |
| Large file upload | Video file | 100 MB - 1 GB |
| Malicious request | Attacker sending garbage data | 1-100 GB |
Client sends request with body
↓
Caddy receives the request
↓
Caddy checks: Is the body size within the limit?
↓
YES → Caddy accepts body, forwards to your app
NO → Caddy rejects request (413 Payload Too Large)
↓
Your Node.js app processes the request (if accepted)
Key point: Size checking happens at Caddy level, before it reaches your application. This protects your app from even receiving oversized requests.
By default, Caddy has no built-in request body size limit. It will accept requests of any size (limited only by available disk space and RAM).
# Default - no limits
example.com {
reverse_proxy localhost:3000
}
Problems:
Attacker uploads 100 GB file
↓
Caddy accepts all 100 GB into memory
↓
Server runs out of RAM
↓
Server crashes
↓
All legitimate users get "Service Unavailable"
Real-world attacks:
Attack 1: Disk Exhaustion
Attacker runs script:
for i in {1..100}; do
curl -X POST --data @1gb-file.bin https://example.com/upload
done
Result:
- 100 GB written to disk
- Disk is full
- Application can't write logs or database records
- Service becomes unavailable
Attack 2: Memory Exhaustion
Attacker sends:
POST /api/submit
Content-Length: 100GB
[100 GB of data]
Caddy starts receiving the request
Memory fills up: 25%, 50%, 75%, 100%
Server becomes sluggish
Application timeout
Attack 3: Bandwidth Waste
Attacker or legitimate user misconfigures client:
Client uploads entire hard drive (500 GB)
Your bandwidth bill: $5,000
Attack 4: Slow Clients
Attacker sends data very slowly (1 byte per second)
Caddy has to keep connection open
After 100,000 slow connections
All connection slots are full
Legitimate users can't connect
Caddy accepts various size units:
| Unit | Meaning | Example |
|---|---|---|
| B | Bytes | 1024B = 1 KB |
| KB | Kilobytes | 1KB = 1,000 bytes |
| MB | Megabytes | 1MB = 1,000 KB |
| GB | Gigabytes | 1GB = 1,000 MB |
| KiB | Kibibytes (binary) | 1KiB = 1,024 bytes |
| MiB | Mebibytes (binary) | 1MiB = 1,024 KB |
Note: Most people use MB (decimal), but MiB (binary) is technically more accurate for computers.
| Use Case | Recommended Limit | Reasoning |
|---|---|---|
| JSON API request | 1 MB | Plenty for structured data |
| Form submission | 5 MB | Covers most normal forms |
| Small file upload | 10 MB | Photos, documents |
| Medium file upload | 100 MB | Videos, large files |
| Large file upload | 1 GB | Professional media |
| Very large (streaming) | 5 GB | Use chunked upload instead |
Apply the same limit to your entire domain.
Limit entire domain to 10 MB:
example.com {
request_body {
max_size 10MB
}
reverse_proxy localhost:3000
}
What this does:
User uploads 5 MB file → ✅ Allowed
User uploads 15 MB file → ❌ Rejected (HTTP 413)
API request with 2 MB JSON → ✅ Allowed
Form submission with 500 KB → ✅ Allowed
HTTP Response When Exceeded:
HTTP/1.1 413 Payload Too Large
Content-Type: text/plain
Payload Too Large
For REST APIs without file uploads, use a strict limit:
api.example.com {
request_body {
max_size 1MB
}
reverse_proxy localhost:3001
}
Protects against:
Accidental large submissions
JSON bomb attacks (deeply nested JSON)
Bulk operation abuse
For applications that accept file uploads:
upload.example.com {
request_body {
max_size 1GB
}
reverse_proxy localhost:3002
}
Allows large file uploads while still preventing excessively large ones.
# Strict API - no file uploads
api.example.com {
request_body {
max_size 1MB
}
reverse_proxy localhost:3001
}
# File service - large uploads
files.example.com {
request_body {
max_size 100MB
}
reverse_proxy localhost:3002
}
# Public website - moderate limit
www.example.com {
request_body {
max_size 10MB
}
reverse_proxy localhost:3000
}
Only apply limits to certain routes. Different endpoints have different needs.
example.com {
handle_path /upload/* {
request_body {
max_size 5MB
}
reverse_proxy localhost:3000
}
handle {
reverse_proxy localhost:3000
}
}
Access behavior:
POST /upload/file → Limited to 5 MB
GET /products → No limit (just gets data)
POST /api/search → No limit (small JSON)
POST /contact → No limit (form is small)
example.com {
# Profile pictures - small files
handle_path /upload/profile-pic/* {
request_body {
max_size 5MB
}
reverse_proxy localhost:3000
}
# Document uploads - moderate size
handle_path /upload/documents/* {
request_body {
max_size 50MB
}
reverse_proxy localhost:3000
}
# Video uploads - larger size
handle_path /upload/videos/* {
request_body {
max_size 500MB
}
reverse_proxy localhost:3000
}
# Everything else - no upload limit
handle {
reverse_proxy localhost:3000
}
}
Usage:
User uploads 3 MB photo → /upload/profile-pic → ✅ Allowed (under 5 MB)
User uploads 10 MB document → /upload/documents → ✅ Allowed (under 50 MB)
User uploads 200 MB video → /upload/videos → ✅ Allowed (under 500 MB)
User uploads 10 MB to /upload/profile-pic → ❌ Rejected (exceeds 5 MB)
api.example.com {
# Bulk import - large payloads
handle_path /api/v1/import/* {
request_body {
max_size 100MB
}
reverse_proxy localhost:3001
}
# Standard API - moderate payloads
handle_path /api/v1/* {
request_body {
max_size 10MB
}
reverse_proxy localhost:3001
}
# Public endpoints - small payloads
handle_path /api/public/* {
request_body {
max_size 1MB
}
reverse_proxy localhost:3001
}
}
example.com {
# File upload with multiple protections
handle_path /upload/* {
# Limit request size
request_body {
max_size 50MB
}
# Rate limit uploads
rate_limit {
zone uploads {
key {remote_host}
events 10
window 1h
}
}
# Require authentication
basicauth {
user $2a$12$abc123...
}
reverse_proxy localhost:3000
}
handle {
reverse_proxy localhost:3000
}
}
shop.example.com {
# Product images - moderate limit
handle_path /upload/products/* {
request_body {
max_size 10MB
}
reverse_proxy localhost:3000
}
# User profile pictures - small limit
handle_path /upload/profile/* {
request_body {
max_size 5MB
}
reverse_proxy localhost:3000
}
# API for mobile app - strict limit
handle_path /api/* {
request_body {
max_size 5MB
}
reverse_proxy localhost:3000
}
# Checkout form - small limit
handle_path /checkout {
request_body {
max_size 1MB
}
reverse_proxy localhost:3000
}
# Everything else
handle {
reverse_proxy localhost:3000
}
}
docs.example.com {
# Document uploads - large files allowed
handle_path /api/documents/upload {
request_body {
max_size 500MB
}
# Require authentication
basicauth {
user $2a$12$abc123...
}
# Rate limit to prevent abuse
rate_limit {
zone uploads {
key {http.request.header.Authorization}
events 20
window 1h
}
}
reverse_proxy localhost:3001
}
# API queries - small payloads
handle_path /api/* {
request_body {
max_size 5MB
}
reverse_proxy localhost:3001
}
handle {
reverse_proxy localhost:3001
}
}
app.example.com {
# Free tier - strict limits
handle_path /api/free/* {
request_body {
max_size 1MB
}
reverse_proxy localhost:3000
}
# Professional tier - moderate limits
handle_path /api/pro/* {
request_body {
max_size 50MB
}
reverse_proxy localhost:3000
}
# Enterprise tier - generous limits
handle_path /api/enterprise/* {
request_body {
max_size 1GB
}
reverse_proxy localhost:3000
}
# File upload - depends on plan
handle_path /upload/* {
@pro_user {
header X-Plan professional
}
@enterprise_user {
header X-Plan enterprise
}
handle @enterprise_user {
request_body {
max_size 1GB
}
reverse_proxy localhost:3000
}
handle @pro_user {
request_body {
max_size 100MB
}
reverse_proxy localhost:3000
}
# Free tier
request_body {
max_size 10MB
}
reverse_proxy localhost:3000
}
handle {
reverse_proxy localhost:3000
}
}
# ❌ WRONG - Too restrictive
example.com {
request_body {
max_size 1KB
}
reverse_proxy localhost:3000
}
Problems:
Users can't submit normal forms (which are usually > 1 KB)
APIs reject legitimate requests
Lots of frustrated users
Solution: Set reasonable limits based on your use case:
# ✅ CORRECT
example.com {
request_body {
max_size 10MB # Reasonable default
}
reverse_proxy localhost:3000
}
# ❌ WRONG - No real protection
example.com {
request_body {
max_size 100GB
}
reverse_proxy localhost:3000
}
Problems:
Doesn't protect against large file attacks
Disk can still fill up
Memory can still be exhausted
Solution: Set realistic limits:
# ✅ CORRECT - Based on actual use case
example.com {
request_body {
max_size 50MB # If handling uploads
}
reverse_proxy localhost:3000
}
# ❌ WRONG - Upload endpoint has no limit
example.com {
request_body {
max_size 10MB
}
handle_path /upload/* {
reverse_proxy localhost:3000
}
handle {
reverse_proxy localhost:3000
}
}
The global limit applies, but it might be too strict for uploads or too generous.
Solution: Explicitly set limits for each endpoint:
# ✅ CORRECT
example.com {
handle_path /upload/* {
request_body {
max_size 500MB # Specific for uploads
}
reverse_proxy localhost:3000
}
handle {
request_body {
max_size 10MB # Default for others
}
reverse_proxy localhost:3000
}
}
# ❌ WRONG - Internal API unprotected
internal.example.com {
reverse_proxy localhost:3000
}
Even internal APIs can be attacked by:
Compromised internal services
Bugs in other services making oversized requests
Intentional abuse by disgruntled employees
Solution: Protect internal endpoints too:
# ✅ CORRECT
internal.example.com {
request_body {
max_size 50MB
}
reverse_proxy localhost:3000
}
# ❌ WRONG - Set a limit without testing
example.com {
request_body {
max_size 5MB
}
reverse_proxy localhost:3000
}
You set 5 MB, but users need to upload 8 MB files. They get rejected. Bad UX.
Solution: Test with actual use cases:
# Test uploading a file
curl -F "file=@large-file.bin" https://example.com/upload
# Test with exactly the size limit
dd if=/dev/zero bs=1M count=5 of=test-file.bin
curl -F "file=@test-file.bin" https://example.com/upload
# Test exceeding the limit
dd if=/dev/zero bs=1M count=6 of=test-file-large.bin
curl -F "file=@test-file-large.bin" https://example.com/upload
# Should get 413 Payload Too Large
# ❌ WRONG - Limits GET which has no body
example.com {
handle_path /api/search {
request_body {
max_size 1MB
}
reverse_proxy localhost:3000
}
}
GET requests (fetching data) don't have bodies, so this limit does nothing. But it's confusing.
Solution: Only set limits on routes that receive bodies (POST, PUT, PATCH):
# ✅ CORRECT - Only on endpoints with bodies
example.com {
handle_path /api/search {
# No limit needed, GET has no body
reverse_proxy localhost:3000
}
handle_path /api/create {
# Limit POST requests
request_body {
max_size 10MB
}
reverse_proxy localhost:3000
}
}
# ❌ INCOMPLETE - Limit per request, but not total
example.com {
request_body {
max_size 500MB
}
reverse_proxy localhost:3000
}
Problems:
Each user can upload 500 MB
If 100 users upload simultaneously, that's 50 GB total
Still fills disk
Solution: Combine with rate limiting:
# ✅ CORRECT - Limit individual uploads AND frequency
example.com {
rate_limit {
zone uploads {
key {remote_host}
events 5 # Max 5 uploads per hour
window 1h
}
}
handle_path /upload/* {
request_body {
max_size 500MB # Per upload
}
reverse_proxy localhost:3000
}
handle {
reverse_proxy localhost:3000
}
}
Enable Caddy logging to track rejected requests:
example.com {
log {
output stdout
format json
}
request_body {
max_size 10MB
}
reverse_proxy localhost:3000
}
View logs:
# Watch for 413 errors
sudo journalctl -u caddy -f | grep "413"
# Or in JSON
docker logs caddy-container | jq 'select(.status==413)'
In your application, warn users about size limits:
// Check file size before upload
document.getElementById('file-input').addEventListener('change', (e) => {
const file = e.target.files[0];
const maxSize = 10 * 1024 * 1024; // 10 MB
if (file.size > maxSize) {
alert(`File too large. Max size: 10 MB, Your file: ${(file.size / 1024 / 1024).toFixed(2)} MB`);
e.target.value = '';
}
});
# Test with 1 MB file
dd if=/dev/urandom bs=1M count=1 of=file-1mb.bin
curl -X POST -F "file=@file-1mb.bin" https://example.com/upload
# Test with 10 MB file
dd if=/dev/urandom bs=1M count=10 of=file-10mb.bin
curl -X POST -F "file=@file-10mb.bin" https://example.com/upload
# Test with 20 MB file (should fail if limit is 10 MB)
dd if=/dev/urandom bs=1M count=20 of=file-20mb.bin
curl -X POST -F "file=@file-20mb.bin" https://example.com/upload
# Should get: 413 Payload Too Large
When users need to upload files larger than reasonable request limits, use chunked uploads:
Instead of uploading one large file, break it into smaller chunks:
async function uploadLargeFile(file, chunkSize = 5 * 1024 * 1024) {
const chunks = Math.ceil(file.size / chunkSize);
for (let i = 0; i < chunks; i++) {
const start = i * chunkSize;
const end = Math.min(start + chunkSize, file.size);
const chunk = file.slice(start, end);
const formData = new FormData();
formData.append('chunk', chunk);
formData.append('chunkIndex', i);
formData.append('totalChunks', chunks);
formData.append('fileId', uniqueFileId);
const response = await fetch('/api/upload-chunk', {
method: 'POST',
body: formData
});
if (!response.ok) {
throw new Error(`Chunk ${i} failed`);
}
}
// All chunks uploaded, tell server to assemble
await fetch('/api/upload-complete', {
method: 'POST',
body: JSON.stringify({ fileId: uniqueFileId })
});
}
example.com {
# Chunk upload endpoint - small chunks
handle_path /api/upload-chunk {
request_body {
max_size 10MB # Each chunk max 10 MB
}
reverse_proxy localhost:3000
}
# Finalize endpoint - no body needed
handle_path /api/upload-complete {
request_body {
max_size 1MB
}
reverse_proxy localhost:3000
}
}
Benefits of chunked upload:
Users can upload files of any size (limited by disk, not request size)
Resume capability (re-upload failed chunks, not whole file)
Better progress indication
Server can validate each chunk
No need to increase request limits excessively
example.com {
log {
output stdout
format json
}
# Profile picture upload - small
handle_path /api/users/profile-picture {
request_body {
max_size 5MB
}
rate_limit {
zone profile_pic {
key {http.request.header.Authorization}
events 10
window 1d
}
}
basicauth {
user $2a$12$abc123...
}
reverse_proxy localhost:3000
}
# Document upload - medium
handle_path /api/documents/upload {
request_body {
max_size 100MB
}
rate_limit {
zone doc_upload {
key {http.request.header.Authorization}
events 20
window 1d
}
}
basicauth {
user $2a$12$abc123...
}
reverse_proxy localhost:3000
}
# Chunk upload for large files
handle_path /api/files/chunk {
request_body {
max_size 10MB
}
rate_limit {
zone chunk_upload {
key {http.request.header.Authorization}
events 1000
window 1h
}
}
basicauth {
user $2a$12$abc123...
}
reverse_proxy localhost:3000
}
# API requests - strict limit
handle_path /api/* {
request_body {
max_size 5MB
}
rate_limit {
zone api {
key {remote_host}
events 1000
window 1m
}
}
reverse_proxy localhost:3000
}
# Public site
handle {
request_body {
max_size 1MB
}
reverse_proxy localhost:3000
}
}
# API Service - strict limits
api.example.com {
request_body {
max_size 5MB
}
reverse_proxy localhost:3001
}
# File Upload Service - generous limits
upload.example.com {
request_body {
max_size 500MB
}
basicauth {
uploader $2a$12$abc123...
}
reverse_proxy localhost:3002
}
# WebSocket Service - no body (streaming)
ws.example.com {
reverse_proxy localhost:3003
}
# Public Website - moderate limits
www.example.com {
request_body {
max_size 10MB
}
reverse_proxy localhost:3000
}
Request size limits are essential for protecting against disk/memory exhaustion attacks.
Set realistic limits based on your actual use cases, not too strict or too generous.
Protect specific endpoints where users upload files, with higher limits than API endpoints.
Combine with other security:
Rate limiting (prevent concurrent abuse)
IP filtering (block known abusers)
Authentication (identify users)
Test your limits before deploying to production.
Monitor rejected requests via logs to identify issues or abuse.
For very large files, use chunked uploads instead of increasing request limits excessively.
Document your limits in API documentation so clients know what to expect.
Consider user experience - don't set limits so strict that legitimate use cases fail.
Think about disk space - limit how much data users can store, not just upload size.
Use this as a starting point for your application:
# Minimal API (no uploads)
request_body { max_size 1MB }
# Standard web app
request_body { max_size 10MB }
# File upload service
request_body { max_size 100MB }
# Video service
request_body { max_size 500MB }
# Professional storage
request_body { max_size 1GB }
# Chunked upload per chunk
request_body { max_size 10MB }

29 Dec 2025
Node.js vs Python: Which is Better for Back-End Development?

25 Dec 2025
Top 5 Animated UI Component Libraries for Frontend Developers

24 Dec 2025
Why Most Modern Apps use Kafka