Skip to content

🏗️ Big File Upload System — High-Level Design

Target Level: Senior/Staff Engineer | Focus: Reliable multi-GB uploads, resumability, async processing, storage optimization


1. SYSTEM OVERVIEW

Purpose: Handle reliable upload of large files (100MB–100GB) with progress tracking, resume capability, virus scanning, and post-processing pipelines.

Scale: 10K concurrent uploads, files up to 100GB, 1M+ files stored

Capacity (derive, don't assert):

Quantity Estimate
Aggregate ingress 10K uploads × ~10 Mbps average client uplink ≈ 100 Gbps ≈ 12.5 GB/s. Far more than app servers should proxy → bytes go direct to S3 via pre-signed part URLs
Chunk PUT rate 12.5 GB/s ÷ 8 MiB parts ≈ 1,500 PUT/s, spread over thousands of {upload_id} prefixes (S3: ~3,500 PUT/s per prefix)
Metadata writes One row update per part ≈ 1,500 writes/s: fine for one Postgres primary; nothing per byte
Part size max(5 MiB, ceil(size / 10,000)): 100 GiB needs ≥ 10.24 MiB parts
Temp storage Incomplete uploads × size; e.g. 10K × 5 GB average = 50 TB in flight, which is why GC and lifecycle rules matter

Users: End users (upload/download), Content moderators (review/approve), System admins (monitor/manage)

Use Cases: Video upload (YouTube/Vimeo), Document upload (Google Drive), Media sharing (Dropbox), Dataset upload (ML platforms), Medical imaging (DICOM)

Constraints: - Upload time within ~1.2× raw transfer time (10 GB at 50 Mbps is ~27 min of pure transfer, so "10 GB in < 30 min on 50 Mbps" leaves almost no headroom) - Resumability: survive network drops up to 7 days - Virus scanning: 100% of uploads scanned < 5 min - Durability inherited from S3 (designed for 11 nines); 99.9% availability for the upload API - Idempotency: no duplicate storage on retry


2. HIGH-LEVEL ARCHITECTURE

┌─────────────────────────────────────────────────────────────────────┐
│                          CLIENT SIDE                                │
│  ┌────────────────┐  ┌────────────────┐  ┌───────────────────────┐  │
│  │  File Splitter  │  │  Chunk Manager │  │  Upload Scheduler    │  │
│  │  (Blob.slice()) │  │  (Queue +      │  │  (Concurrency 3-6)   │  │
│  │                 │  │   Retry Logic) │  │                       │  │
│  └────────┬───────┘  └───────┬────────┘  └──────────┬────────────┘  │
│           │                  │                      │               │
│           └──────────────────┴──────────────────────┘               │
│                              │                                      │
│                   ┌──────────▼──────────┐                           │
│                   │  TUS Client / SDK   │                           │
│                   │  (tus-js-client)    │                           │
│                   └──────────┬──────────┘                           │
└──────────────────────────────┼──────────────────────────────────────┘
                               │
                               ▼
┌──────────────────────────────────────────────────────────────────────┐
│                      API GATEWAY (Kong/NGINX)                       │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌────────────────────┐  │
│  │ Auth     │  │ Rate     │  │ Request  │  │ TLS Termination   │  │
│  │ (JWT)    │  │ Limiting │  │ Validation│  │ + Load Balancing  │  │
│  └──────────┘  └──────────┘  └──────────┘  └────────────────────┘  │
└──────────────────────────────┬──────────────────────────────────────┘
                               │
                               ▼
┌──────────────────────────────────────────────────────────────────────┐
│                     UPLOAD SERVICE (FastAPI/Python)                 │
│                                                                     │
│  ┌────────────────┐  ┌────────────────┐  ┌──────────────────────┐   │
│  │ Initiate       │  │ Chunk Receive  │  │ Complete Assembly   │   │
│  │ (POST /upload) │  │ (PATCH /upload │  │ (POST /upload/{id}) │   │
│  │                │  │  /{id}/chunk)  │  │                      │   │
│  └───────┬────────┘  └───────┬────────┘  └──────────┬───────────┘   │
│          │                  │                       │                │
│          └──────────────────┴───────────────────────┘                │
│                              │                                       │
│                   ┌──────────▼──────────┐                            │
│                   │  Presigned URL Gen  │                            │
│                   │  (S3 STS / Vault)   │                            │
│                   └──────────┬──────────┘                            │
└──────────────────────────────┼──────────────────────────────────────┘
                               │
           ┌───────────────────┼───────────────────┐
           ▼                   ▼                   ▼
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│  METADATA DB     │ │  OBJECT STORE    │ │  MESSAGE QUEUE   │
│  (PostgreSQL)    │ │  (S3/MinIO)      │ │  (Kafka/RMQ)     │
│                  │ │                  │ │                  │
│  • uploads       │ │  • temp-chunks/  │ │  • file.uploaded │
│  • chunks        │ │  • permanent/    │ │  • file.scanned  │
│  • files         │ │  • quarantine/   │ │  • file.ready    │
│  • users         │ │  • thumbnails/   │ │                  │
└──────────────────┘ └──────────────────┘ └────────┬─────────┘
                                                   │
                                                   ▼
                              ┌────────────────────────────────────────┐
                              │        ASYNC PROCESSING PIPELINE       │
                              │                                        │
                              │  ┌────────────────────────────────┐   │
                              │  │  VIRUS SCANNER (ClamAV + YARA) │   │
                              │  │  → clean: move to permanent    │   │
                              │  │  → infected: move to quarantine│   │
                              │  └────────────┬───────────────────┘   │
                              │               │                        │
                              │  ┌────────────▼───────────────────┐   │
                              │  │  TRANSCODER (FFmpeg)           │   │
                              │  │  → resolutions (1080p, 720p…)  │   │
                              │  │  → thumbnails + HLS segments   │   │
                              │  └────────────┬───────────────────┘   │
                              │               │                        │
                              │  ┌────────────▼───────────────────┐   │
                              │  │  NOTIFIER (WebSocket/Webhook)   │   │
                              │  │  → user notified on completion  │   │
                              │  └────────────────────────────────┘   │
                              └────────────────────────────────────────┘

Figure: client, upload service, storage and the async processing pipeline.

flowchart TB
  CL["Client: splitter, chunk manager, TUS client"] --> GW["API gateway: auth, rate limit, TLS"]
  GW --> US["Upload service (FastAPI)"]
  US --> DB[("Metadata DB: PostgreSQL")]
  US --> OS[("Object store: S3 / MinIO")]
  US --> MQ["Message queue: file.uploaded"]
  MQ --> VS["Virus scanner"]
  VS --> TR["Transcoder"]
  TR --> NO["Notifier"]

3. DETAILED DESIGN FLOW

3.1 Upload Lifecycle State Machine

                  ┌─────────────────────────┐
                  │       INITIATED          │
                  │  (metadata created,      │
                  │   upload_id returned)    │
                  └────────────┬─────────────┘
                               │ first chunk received
                               ▼
                  ┌─────────────────────────┐
                  │      IN_PROGRESS         │
                  │  (chunks being uploaded) │
                  │  (can be PAUSED/         │
                  │   RESUMED via offset)   │
                  └────────────┬─────────────┘
                               │ all chunks complete
                               ▼
                  ┌─────────────────────────┐
                  │      COMPLETED           │
                  │  (chunks assembled,      │
                  │   file integrity check) │
                  └────────────┬─────────────┘
                               │ queued for processing
                               ▼
                  ┌─────────────────────────┐
                  │     PROCESSING           │
                  │  (virus scan, transcode) │
                  └──────┬──────────┬───────┘
                    clean │          │ infected
                          ▼          ▼
              ┌──────────────┐ ┌──────────────┐
              │    READY     │ │  QUARANTINED │
              │ (available   │ │ (flagged for │
              │  for download│ │  review)     │
              └──────────────┘ └──────────────┘

Figure: upload lifecycle states.

stateDiagram-v2
  [*] --> INITIATED
  INITIATED --> IN_PROGRESS: first chunk
  IN_PROGRESS --> COMPLETED: all chunks, integrity ok
  COMPLETED --> PROCESSING: queued
  PROCESSING --> READY: clean
  PROCESSING --> QUARANTINED: infected

3.2 Step-by-Step Upload Flow

Client                Upload Service           Metadata DB          Object Store
  │                         │                      │                   │
  │──── POST /upload ──────►│                      │                   │
  │   {filename, size,      │                      │                   │
  │    mime_type, checksum} │                      │                   │
  │                         │── INSERT upload ────►│                   │
  │                         │   (status=INITIATED) │                   │
  │◄─── {upload_id,         │                      │                   │
  │      chunk_size,        │  (≥ 5 MiB and ≥ size/10,000)             │
  │      total_chunks}      │                      │                   │
  │                         │                      │                   │
  │ ═══════ UPLOAD CHUNKS (parallel, up to 6) ════════                │
  │                         │                      │                   │
  │──── PATCH /upload/{id} ─►                      │                   │
  │   /chunk?offset=0       │                      │                   │
  │   [chunk_data]          │                      │                   │
  │                         │── PUT chunk ────────────────────────────►│
  │                         │   blob/{id}/chunk_0  │                   │
  │                         │── UPDATE chunk ─────►│                   │
  │◄─── {offset: 0,         │   {chunk_num=0,      │                   │
  │      received: 5242880, │    size=5MB,          │                   │
  │      checksum: <sha256>}│    checksum_matched}  │                   │
  │                         │                      │                   │
  │ (network drops here!)   │                      │                   │
  │                         │                      │                   │
  │──── HEAD /upload/{id} ──►                      │                   │
  │                         │── SELECT received ───►│                   │
  │                         │   chunk numbers       │                   │
  │◄─── {offset: <end of contiguous prefix>,       │                   │
  │      missing: [1, 4, …]}│                      │                   │
  │   (NOT max(offset): with parallel chunks there can be gaps below it)│
  │                         │                      │                   │
  │──── PATCH /upload/{id} ─►                      │                   │
  │   /chunk?offset=5242880 │                      │                   │
  │   [next_chunk_data]     │                      │                   │
  │                         │── PUT chunk ────────────────────────────►│
  │ (repeated for all       │                      │                   │
  │  chunks)                │                      │                   │
  │                         │                      │                   │
  │──── POST /upload/{id} ──►                      │                   │
  │   /complete             │                      │                   │
  │   {checksum: <sha256>,  │                      │                   │
  │    total_chunks: N}     │                      │                   │
  │                         │── Verify all chunks ─►│                  │
  │                         │── Assemble (multipart ──────────────────►│
  │                         │   upload complete)    │                   │
  │                         │── UPDATE status ─────►│                   │
  │                         │   =COMPLETED          │                   │
  │◄─── {status: "completed",                       │                   │
  │      file_id: "f_xxx",                          │                   │
  │      size: 1.2GB}                               │                   │
  │                         │                      │                   │
  │                         │── PUBLISH ───────────────────────────────►│
  │                         │   file.uploaded → Kafka                  │

Figure: initiate, parallel chunk upload, resume after a network drop, and complete.

sequenceDiagram
  participant C as Client
  participant U as Upload service
  participant D as Metadata DB
  participant S as Object store
  C->>U: POST /upload (filename, size, checksum)
  U->>D: INSERT upload INITIATED
  U-->>C: upload_id, chunk_size
  loop chunks (parallel)
    C->>U: PATCH chunk
    U->>S: PUT chunk
    U->>D: UPDATE chunk
    U-->>C: received, checksum
  end
  Note over C,U: Network drops
  C->>U: HEAD /upload/id
  U-->>C: contiguous offset, missing chunks
  C->>U: POST complete (checksum)
  U->>S: Assemble multipart
  U->>D: status COMPLETED
  U--)S: Publish file.uploaded

3.3 Async Processing Flow

Kafka: file.uploaded ──► Virus Scanner ──► clean ──► Transcoder ──► Notifier
                                                      │
                                 ┌────────────────────┤
                                 ▼                    ▼
                          ┌──────────────┐   ┌──────────────┐
                          │  Thumbnails  │   │  HLS Segments│
                          │  (320, 640,  │   │  (240p, 480p,│
                          │   1280, 1920)│   │   720p, 1080p)│
                          └──────────────┘   └──────────────┘
                                                      │
                                                      ▼
                          ┌──────────────────────────────┐
                          │  CDN Invalidation + Cache    │
                          │  Warm (CloudFront / Cloudflare│
                          └──────────────────────────────┘

4. KEY COMPONENTS

4.1 Upload Service (Python/FastAPI)

Endpoint Method Purpose
/api/v1/upload POST Initiate new upload — returns upload_id, chunk_size, max_chunks
/api/v1/upload/{id}/chunk PATCH Upload a chunk at a specific offset (TUS-compatible)
/api/v1/upload/{id} HEAD Get current upload offset (for resume)
/api/v1/upload/{id}/complete POST Finalize upload — verify checksum, assemble, queue processing
/api/v1/upload/{id}/status GET Get upload status (progress, ETA, processing status)
/api/v1/upload/{id} DELETE Cancel/abort upload — clean up partial chunks

4.2 Metadata Database Schema (PostgreSQL)

-- Upload sessions
CREATE TABLE uploads (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id         UUID NOT NULL REFERENCES users(id),
    filename        TEXT NOT NULL,
    file_size       BIGINT NOT NULL,           -- Total file size in bytes
    mime_type       TEXT NOT NULL,
    checksum        TEXT,                       -- Client-provided SHA-256
    chunk_size      INT NOT NULL DEFAULT 5242880,  -- 5MB default
    total_chunks    INT NOT NULL,
    status          TEXT NOT NULL DEFAULT 'initiated'
                    CHECK (status IN ('initiated','in_progress','assembling','completed',
                                      'ready','quarantined','failed','cancelled','expired')),
    storage_key     TEXT,                       -- S3 object key
    storage_upload_id TEXT,                     -- S3 multipart UploadId
    storage_bucket  TEXT,
    created_at      TIMESTAMPTZ DEFAULT NOW(),
    completed_at    TIMESTAMPTZ,
    expires_at      TIMESTAMPTZ DEFAULT NOW() + INTERVAL '7 days'   -- bumped on every chunk
);

-- Chunk tracking
CREATE TABLE chunks (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    upload_id       UUID NOT NULL REFERENCES uploads(id) ON DELETE CASCADE,
    chunk_number    INT NOT NULL,
    offset_start    BIGINT NOT NULL,
    offset_end      BIGINT NOT NULL,
    size            INT NOT NULL,
    checksum        TEXT,                       -- SHA-256 of chunk
    storage_path    TEXT,                       -- blob/{upload_id}/chunk_{n}
    received_at     TIMESTAMPTZ DEFAULT NOW(),
    UNIQUE(upload_id, chunk_number)
);

-- Processed file metadata
CREATE TABLE files (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    upload_id       UUID NOT NULL REFERENCES uploads(id),
    storage_key     TEXT NOT NULL,
    size            BIGINT NOT NULL,
    checksum        TEXT,
    mime_type       TEXT NOT NULL,
    virus_status    TEXT DEFAULT 'pending'
                    CHECK (virus_status IN ('pending','clean','infected')),
    processing_status TEXT DEFAULT 'pending'
                    CHECK (processing_status IN ('pending','processing','ready','failed')),
    thumbnail_keys  JSONB,                      -- {320: "path_320", 640: "path_640"}
    manifest_key    TEXT,                       -- HLS manifest path
    created_at      TIMESTAMPTZ DEFAULT NOW()
);

-- Indexes for performance
CREATE INDEX idx_uploads_user_status ON uploads(user_id, status);
CREATE INDEX idx_uploads_expires ON uploads(expires_at) WHERE status IN ('initiated','in_progress');
CREATE INDEX idx_chunks_upload ON chunks(upload_id, chunk_number);
CREATE INDEX idx_files_status ON files(processing_status) WHERE virus_status = 'clean';

4.3 Object Storage Layout (S3/MinIO)

bucket/
├── temp-chunks/
│   └── {upload_id}/
│       ├── chunk_0
│       ├── chunk_1
│       └── ...
├── permanent/
│   └── {user_id}/
│       └── {file_id}_{filename}
├── quarantine/
│   └── {timestamp}_{file_id}
├── thumbnails/
│   └── {file_id}/
│       ├── 320.webp
│       ├── 640.webp
│       ├── 1280.webp
│       └── 1920.webp
├── hls/
│   └── {file_id}/
│       ├── master.m3u8
│       ├── 240p/
│       ├── 480p/
│       ├── 720p/
│       └── 1080p/
└── av-scan/
    └── {file_id}.scan_result

Lifecycle Policies: - temp-chunks/: Auto-delete after 14 days, and AbortIncompleteMultipartUpload after 14 days. Lifecycle rules count from object creation, not last activity, so they must be longer than the app's 7-day sliding TTL; otherwise S3 deletes chunks of an upload the user resumed on day 6. The app GC is primary, lifecycle is the backstop. - quarantine/: Auto-delete after 30 days - thumbnails/ and hls/: Retain indefinitely


5. DESIGN PATTERNS & TRADE-OFFS

Strategy: Direct-to-Storage (Pre-signed URLs)

Approach: Client uploads directly to S3 via pre-signed URLs
          1. Server generates short-lived (15 min) pre-signed URL
          2. Client PUTs chunk directly to S3
          3. S3 triggers Lambda/notification on completion

Pros:
  - Server is not a bottleneck — no bandwidth consumed
  - Scales with S3 (essentially unlimited)
  - Lower latency for client (direct connection)

Cons:
  - More complex client logic
  - Pre-signed URL expiry management
  - S3 event notifications are asynchronous and at-least-once (usually seconds,
    occasionally longer); S3 reads themselves are strongly consistent
  - The URL fixes the key, not the content: verify size/checksums (ListParts)
    before calling CompleteMultipartUpload

When to use:
  - Files > 100MB
  - High upload concurrency
  - Want to minimize server resource usage

Strategy: Proxy Upload (via App Server)

Approach: Client sends all data through the upload service
          1. Server receives full/chunked data
          2. Server writes to S3

Pros:
  - Simpler client (just POST)
  - Full control over validation and transformation
  - Can enforce business logic mid-stream

Cons:
  - Server is bottleneck (network I/O, memory)
  - Requires horizontal scaling of app servers
  - Higher cost (server bandwidth is expensive)

When to use:
  - Files < 100MB
  - Need complex in-flight validation
  - Low concurrency requirements

Chunking Strategy Comparison

Strategy Description Best For
Fixed-size chunks All chunks same size (e.g., 5MB) Simplicity, predictable storage
Variable-size chunks First/last chunk smaller Streaming, media files
Adaptive chunking Adjust size based on network Mobile, unreliable connections
TUS Protocol Standard HTTP PATCH/HEAD Interoperability, resumability

Storage Strategy: Multipart Upload vs Single Object

Multipart Upload (S3):
  - Upload parts in parallel
  - Upload parts in any order
  - Resume from failed parts
  - Part size: 5 MiB – 5 GiB (the last part may be smaller)
  - Max parts: 10,000 → 5 MiB parts cap the object at ~48.8 GiB;
    for 100 GiB use ≥ 10.24 MiB parts
  - Max object: 5 TiB
  - Per-part checksums (x-amz-checksum-sha256 / crc32c) are verified by S3

Single Object Upload:
  - Simple PUT request
  - Max 5GB in single operation
  - No partial failure recovery
  - Lower overhead for small files

Decision:
  - Files < 100MB: Single PUT (or pre-signed URL)
  - Files > 100MB: Multipart upload via S3 API
  - Files > 5GB: MUST use multipart upload

6. RESILIENCE & ERROR HANDLING

Retry Strategy

RETRY_BACKOFF = {
    "chunk_upload": {
        "max_retries": 3,
        "base_delay": 1.0,  # seconds
        "max_delay": 30.0,
        "backoff_factor": 2.0,  # exponential
    },
    "complete_upload": {
        "max_retries": 5,
        "base_delay": 2.0,
        "max_delay": 60.0,
    },
    "processing": {
        "max_retries": 3,
        "base_delay": 5.0,  # virus scan transient failures
    }
}

Error Types & Handling

Error Client Action Server Action
Network timeout Retry chunk with exponential backoff Accept duplicate chunk (idempotent)
401 Unauthorized Refresh token, retry Revoke old pre-signed URLs
409 Conflict Re-initiate upload Clean up stale chunks
413 Payload Too Large Reject client-side Return max_allowed_size
500 Server Error Retry with backoff Log, alert on-call
Checksum mismatch Re-upload corrupted chunk Delete corrupted chunk

Garbage Collection

# Background job — runs every hour
async def cleanup_abandoned_uploads():
    """Delete chunks for uploads that were never completed."""
    # Claim and mark in ONE statement. (A bare SELECT ... FOR UPDATE SKIP LOCKED
    # in autocommit releases its locks immediately, so two GC workers would
    # both process the same rows.) Use expires_at, which slides with activity,
    # not created_at, or you expire uploads that are actively resuming.
    stale_uploads = await db.fetch("""
        UPDATE uploads SET status = 'expired'
        WHERE id IN (SELECT id FROM uploads
                     WHERE status IN ('initiated', 'in_progress')
                       AND expires_at < NOW()
                     LIMIT 500
                     FOR UPDATE SKIP LOCKED)
        RETURNING id
    """)
    for upload in stale_uploads:
        await storage.delete_prefix(f"temp-chunks/{upload.id}/")
        await db.execute("DELETE FROM chunks WHERE upload_id = $1", upload.id)
        # (and AbortMultipartUpload if storage_upload_id is set; release the
        #  user's concurrency slot and refund reserved quota)

Failure Modes

Failure What happens Handling
Client crashes / laptop closes Upload stays in_progress Client persisted upload_id; on restart it asks for missing parts and sends only those
Same part sent twice (retry after a lost ack) Second write is a duplicate Deterministic key + ON CONFLICT DO NOTHING; same checksum → success, different → 409
Two concurrent requests for one part Race on the same key In-flight reservation (or the unique constraint); loser gets a retryable 409
complete races a part upload Could assemble without a part the client thinks it sent Refuse complete while parts are in flight; assembling state blocks new writes
Storage error during assembly Upload stranded in assembling Roll back to in_progress (parts intact); client retries complete
Lost response to complete Client retries complete is idempotent: already completed → same response
Crash between "status = completed" and "publish file.uploaded" Scan never runs Transactional outbox in the same DB transaction; relay to Kafka; idempotent consumers
Corruption at rest Assembled file wrong S3-side part checksums; re-verify failing parts and ask the client to re-send them
S3 503 SlowDown Part PUTs fail Exponential backoff + jitter on the client, more retries than 3; keys spread over many prefixes
Abandoned uploads Temp storage grows Sliding-TTL GC + lifecycle backstop (longer than the TTL)

7. SECURITY CONSIDERATIONS

Threat Mitigation
Malicious file upload File type validation (magic bytes, not just extension) + virus scanning
Upload bombing Rate limit per user (10 concurrent uploads, 1GB/min)
Path traversal Sanitize filenames, use UUID-based storage paths
Pre-signed URL abuse Short TTL (15 min), scope to specific upload_id + chunk_number
Unrestricted upload size Reject files > max_allowed_size (100GB default)
SSRF via upload URL Validate all redirect URLs, use allowlist
Storage access bypass Signed URLs for downloads with expiry
Stored XSS via uploaded HTML/SVG Serve user files from a separate cookieless domain with Content-Disposition: attachment and X-Content-Type-Options: nosniff
Decompression bombs Cap expanded size and file count when scanning or unpacking archives
Content-type spoofing Sniff magic bytes server-side; never trust the client's Content-Type

8. SCALABILITY

Bottlenecks & Solutions

Bottleneck Limit Solution
Upload Service CPU Request handling Auto-scale pods (HPA: CPU > 70%)
Metadata DB writes Concurrent chunk tracking Partition by upload_id, connection pooling
Object Store PUTs S3 bucket limits Use prefix sharding ({upload_id:0-3}/...)
Virus Scanner Scan throughput (500MB/min per node) Parallel scanning workers, separate queue
Transcoder CPU/GPU for encoding Auto-scale transcoder pool with priority queue

Horizontal Scaling

LOAD BALANCER (Round Robin)
    │
    ├── Upload Service Pod 1 (CPU: 40%)
    ├── Upload Service Pod 2 (CPU: 35%)
    ├── Upload Service Pod 3 (CPU: 55%)
    └── Upload Service Pod N (scales to N)
            │
            ▼
    PostgreSQL Read Replicas
    └── Writes go to Primary
    └── Reads (status checks) go to Replicas

Cost Estimation (Monthly — 10TB upload volume)

Component Cost
Upload Service (4 × t3.large) $600
Metadata DB (RDS PostgreSQL) $400
Object Storage (S3 — 10TB stored + 10TB uploaded) $500
Virus Scanning (Lambda × 100K files) $100
Transcoding (Spot instances) $800
CDN (CloudFront — 50TB egress) $4,000
Message Queue (Kafka — 2 brokers) $400
Total ~$6,800

9. MONITORING & ALERTS

Key Metrics (RED)

Rate:
  - upload_initiations_per_second
  - chunks_uploaded_per_second
  - files_processed_per_second

Errors:
  - upload_failure_rate (SLO: < 1%)
  - chunk_checksum_mismatch_rate
  - virus_infection_rate (alert if > 0.1%)
  - processing_pipeline_failure_rate

Duration:
  - p50/p95/p99 upload_completion_time
  - p50/p95/p99 chunk_upload_latency
  - p50/p95/p99 virus_scan_duration
  - p95 processing_pipeline_latency

Dashboards

Upload Dashboard:
  - Active concurrent uploads (current + trend)
  - Upload throughput (MB/s)
  - Success rate by file size bracket
  - Top 5 errors by type

Storage Dashboard:
  - Total stored bytes (by tier)
  - Object count by bucket prefix
  - Lifecycle expiration rate
  - Glacier transition rate

Processing Dashboard:
  - Queue depth (pending files)
  - Scan/completion rate
  - Infected file count (alert)
  - Transcoding queue wait time

10. INTERVIEW DEEP-DIVE QUESTIONS

See INTERVIEW_QUESTIONS.md for a comprehensive set of staff-level interview questions covering this design.