Backend Architecture•July 14, 2026•8 min read

Handling 5GB+ Chunked File Uploads in Django with Background Processing

Nguyen Dai Long

Nguyen Dai Long

Backend Lead & Software Engineer

Uploading multi-gigabyte media files or heavy datasets over volatile mobile or enterprise networks poses severe engineering hurdles. In this article, I dissect our chunked file upload architecture that successfully processes files up to 5GB with automatic pause, resume, and async merging.
Production Environment & Benchmark Specifications
Deployment Architecture

Django 5.0 + Celery Worker Pool + Google Cloud Storage

Active Production Traffic

Average 1,200 heavy files uploaded daily (Sizes ranging 100MB - 5.2GB)

Hardware Specs

API Nodes: 2 vCPU 4GB RAM | Celery Workers: 4 vCPU 8GB RAM with NVMe scratch space

Relational Database

PostgreSQL 16 (metadata) + Redis 7 (chunk tracking bitmap)

Caching & In-Memory Layer

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 ]
Empirical Benchmark Results (Before vs. After Optimization)
Performance MetricBeforeAfter OptimizationImprovementMeasurement Tool
Upload Success Rate (>2GB files)63.4% (TCP drop failures)99.85%+36.45% reliabilitySentry & Custom Ingestion Logs
API Worker RAM Usage4.2GB (OOM Crashes)180MB stable95.7% memory savedPrometheus cAdvisor
Resume Time on Connection DropRestart from 0% (Avg 14 min)Resumes current 5MB chunk (<2s)Client File Slice API
Total Pipeline Throughput12 uploads/worker concurrency150 uploads/worker concurrency12.5x throughputLocust 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.

python
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.

python
@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.
Production Incident Case Study: Out-Of-Memory (OOM) Kernel Panic on 4GB Video Upload

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)

5MB is the universal minimum part size for AWS S3 and Google Cloud Storage multipart uploads. Smaller chunks (e.g. 1MB) introduce excessive HTTP roundtrip overhead; larger chunks (e.g. 50MB) increase user wait time if a chunk fails and needs to be re-transmitted over mobile networks.

Engineering Feedback0 likes

Was this technical breakdown helpful for your production workflow?

Nguyen Dai Long

Written by Nguyen Dai Long

Backend Engineer & Backend Lead with 4+ years of hands-on experience building production systems, RESTful APIs, and cloud infrastructure using Python (Django), Laravel, PostgreSQL, and Google Cloud Platform.

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.

Loading discussion...
NDL Ecosystem

Explore Free Web Tools & Games

View All Tools & Apps