When reverse proxying multi-gigabyte video or AI dataset files from MinIO or AWS S3 object storage, standard Nginx reverse proxy caching exhibits severe limitations. If a video player requests a Byte-Range chunk (e.g. Range: bytes=0-1048575 for the first 1MB of a 4GB file), standard proxies either force a full 4GB download into local disk cache or bypass caching entirely (HTTP 206 Partial Content cache miss).
During a video streaming launch, local cache disk storage flooded in minutes due to full-file caching, causing severe S3 egress bandwidth bills and high p99 latencies.
# Cache Invalidation Penalty on Byte-Range Queries [21:40:02] Client requests 1MB Range (bytes=0-1048575) of 4GB video [21:40:02] Standard Nginx proxy_cache -> DOWNLOADS FULL 4GB FILE FROM MINIO! Result: Local NVMe cache exhausted -> MinIO S3 Network Egress Saturation!
To solve the byte-range caching dilemma, Nginx provides the ngx_http_slice_module.
[ Client Request: Range 0-1MB ]
│
▼
┌───────────────────────────────────────────┐
│ Nginx Gateway (ngx_http_slice_module) │
│ ├── Check Cache Key: $uri$args$slice_rng │
│ ├── Cache HIT ──> Return 1MB to Client │
│ └── Cache MISS ──> Fetch 1MB from MinIO │
└────────────────────┬──────────────────────┘
│ (GET 1MB Sub-Slice)
▼
[ MinIO S3 Cluster ]
The slice module intercepts incoming client Range requests and breaks large files into fixed-size sub-requests (e.g., 1MB slices). Each 1MB slice is cached independently in NVMe disk cache using the cache key $uri$is_args$args$slice_range. When a client requests a specific byte offset, Nginx fetches and caches ONLY the required 1MB slices from S3!
Configure Nginx sub-range caching for MinIO/S3 object storage:
http {
# ----------------------------------------------------------------------
# Cache Zone Allocation (Fast NVMe Disk Path)
# ----------------------------------------------------------------------
proxy_cache_path /var/cache/nginx/s3_slice
levels=1:2
keys_zone=s3_slice_cache:100m
max_size=200g
inactive=7d
use_temp_path=off;
upstream minio_s3_cluster {
server 10.0.30.10:9000 max_fails=3 fail_timeout=10s;
server 10.0.30.11:9000 max_fails=3 fail_timeout=10s;
keepalive 64;
}
server {
listen 80 reuseport;
server_name media.zhabrosima.com;
location /assets/ {
# 1. Enable 1MB Slice Sub-Requests
slice 1m;
# 2. Key Includes $slice_range for independent chunk caching
proxy_cache s3_slice_cache;
proxy_cache_key $uri$is_args$args$slice_range;
proxy_set_header Range $slice_range;
proxy_set_header If-Range $http_if_range;
# 3. Cache valid HTTP 200 and 206 Partial Content responses
proxy_cache_valid 200 206 30d;
proxy_cache_use_stale error timeout updating;
proxy_cache_revalidate on;
# 4. Enable Background Cache Lock to prevent stampedes
proxy_cache_lock on;
proxy_cache_lock_timeout 5s;
proxy_pass http://minio_s3_cluster;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host minio.internal;
# Hide S3 internal metadata headers from external clients
proxy_hide_header x-amz-request-id;
proxy_hide_header x-amz-id-2;
}
}
}
Inspect S3 slice cache hits and sub-range byte transfers using CLI tools:
# Send Byte-Range query and inspect X-Cache status header
curl -i -H "Range: bytes=0-1048575" http://media.zhabrosima.com/assets/video.mp4
# Expected Output:
# HTTP/1.1 206 Partial Content
# Content-Range: bytes 0-1048575/4294967296
# X-Cache-Status: HIT
When updating an existing object on MinIO, disparate cached 1MB slices can cause data corruption if client requests span old and new versions.
ETag across all sub-slice requests.ngx_cache_purge module to selectively clear $uri* slice keys upon S3 ObjectCreated:Put webhooks.We benchmarked 4GB video asset streaming under 10,000 range queries:
| Caching Strategy | S3 Egress Bandwidth Used | NVMe Local Disk Usage | p99 First Byte Latency (TTFB) |
|---|---|---|---|
| Standard Whole-File Caching | 42.1 TB Egress | 200 GB (Disk Full in 5m) | 1,240 ms |
| Nginx 1MB Sub-Range Slicing | 1.2 TB Egress | 14.2 GB (High Cache Efficiency) | 4.2 ms |
| Performance Impact | -97.1% Egress Bandwidth | -92.9% Disk Usage | -99.6% TTFB Drop |
sum(rate(nginx_http_requests_total{cache_status="HIT"}[5m])) / sum(rate(nginx_http_requests_total[5m])) * 100node_filesystem_free_bytes{mountpoint="/var/cache/nginx"}