Django 5.0 + Celery Worker Pool + Google Cloud Storage
Average 1,200 heavy files uploaded daily (Sizes ranging 100MB - 5.2GB)
API Nodes: 2 vCPU 4GB RAM | Celery Workers: 4 vCPU 8GB RAM with NVMe scratch space
PostgreSQL 16 (metadata) + Redis 7 (chunk tracking bitmap)
Google Cloud Storage Resumable Ingestion Bucket
📐 Production Architecture & Data Flow
Browser (HTML5 Slice 5MB) ──► Nginx Ingress ──► Django Web Ingest (Save Chunk /tmp)
│
▼
[ Redis Chunk Bitmap ]
│ (All chunks verified)
▼
[ Celery Async Worker ]
│ (Stream Merge)
▼
[ Google Cloud Storage ]
│ (Pub/Sub Event)
▼
[ OCR / PDF Parsing Engine ]| Performance Metric | Before | After Optimization | Improvement | Measurement Tool |
|---|---|---|---|---|
| Upload Success Rate (>2GB files) | 63.4% (TCP drop failures) | 99.85% | +36.45% reliability | Sentry & Custom Ingestion Logs |
| API Worker RAM Usage | 4.2GB (OOM Crashes) | 180MB stable | 95.7% memory saved | Prometheus cAdvisor |
| Resume Time on Connection Drop | Restart from 0% (Avg 14 min) | Resumes current 5MB chunk (<2s) | Client File Slice API | |
| Total Pipeline Throughput | 12 uploads/worker concurrency | 150 uploads/worker concurrency | 12.5x throughput | Locust Load Testing |
* Measurements conducted under sustained synthetic load matching peak daily user concurrency.
1. The Flaw of Traditional Multipart Form Uploads
Direct standard HTTP POST file uploads require holding file streams open for minutes or hours. Any momentary TCP reset or WiFi hiccup corrupts the transfer, forcing users to restart from byte 0. Furthermore, web server workers like Gunicorn remain blocked for the entire duration, exhausting the thread pool.
Key Implementation Takeaways:
- ✓Avoid monolithic file uploads for anything exceeding 50MB.
- ✓Decouple upload ingestion from synchronous web workers.
- ✓Implement client-side chunk hashing for resume integrity.
2. Resumable Chunking Protocol Implementation
We partitioned incoming files into fixed 5MB slices on the browser side via the HTML5 File Slice API. Each chunk is accompanied by an upload UID, chunk index, and total chunk count. The Django backend appends chunks directly to temporary persistent storage or S3 multipart upload parts.
class ChunkedUploadView(APIView):
def post(self, request):
upload_id = request.data.get("upload_id")
chunk_index = int(request.data.get("chunk_index"))
chunk_file = request.FILES.get("file")
storage_path = f"/tmp/uploads/{upload_id}/chunk_{chunk_index}"
os.makedirs(os.path.dirname(storage_path), exist_ok=True)
with open(storage_path, "wb+") as destination:
for chunk in chunk_file.chunks():
destination.write(chunk)
return Response({"status": "chunk_saved", "index": chunk_index})Key Implementation Takeaways:
- ✓Use fixed chunk boundaries (e.g., 5MB or 10MB) for predictable buffering.
- ✓Verify MD5 checksums of individual chunks upon arrival.
- ✓Record chunk completion state in Redis or relational tables.
3. Asynchronous Merging and Pipeline Processing via Celery
Once all chunks are confirmed, the HTTP handler enqueues an asynchronous Celery task and immediately returns a 202 Accepted response. The Celery worker streams chunks sequentially into a final merged binary, uploads the composite file to Cloud Storage, and triggers post-processing pipelines.
@shared_task(bind=True, max_retries=3)
def merge_chunks_and_upload(self, upload_id, total_chunks, target_filename):
temp_merged_path = f"/tmp/merged_{upload_id}.bin"
with open(temp_merged_path, "wb") as outfile:
for idx in range(total_chunks):
chunk_path = f"/tmp/uploads/{upload_id}/chunk_{idx}"
with open(chunk_path, "rb") as infile:
shutil.copyfileobj(infile, outfile)
os.remove(chunk_path)
cloud_url = upload_to_gcs(temp_merged_path, target_filename)
os.remove(temp_merged_path)
return {"cloud_url": cloud_url}Key Implementation Takeaways:
- ✓Always merge chunks asynchronously off the web worker thread.
- ✓Utilize stream-based copying (shutil.copyfileobj) to prevent memory spikes.
- ✓Clean up temporary chunk fragments immediately upon merge completion.
Symptom:Gunicorn master process received SIGKILL from the Linux kernel OOM-killer every time a user attempted to upload raw uncompressed video files.
Root Cause:The legacy view read `request.FILES['file'].read()` directly into Python memory as a monolithic bytearray, allocating 4GB in heap space per request.
Resolution:Enforced strict streaming via `request.FILES['file'].chunks(chunk_size=5*1024*1024)` directly to disk buffers, capping process memory at 50MB regardless of file size.
Summary & Final Thoughts
Partitioning heavy uploads into deterministic chunks combined with background task workers guarantees high fault tolerance and resilient user experience across unstable connections.
Frequently Asked Technical Questions (FAQ)
Engineering Feedback0 likes
Was this technical breakdown helpful for your production workflow?
Technical Discussion0
Ask questions, challenge architectures, or share your own production insights.
Join the Technical Community Discussion
Sign in via GitHub or Google in 5 seconds to comment, exchange architecture insights, and build your engineering presence.