Skip to content
HomeWorkProcessAboutBlogContact[email protected]
← Back to blog

Building File Upload and Storage Systems for Web Apps

Building File Upload and Storage Systems for Web Apps. file upload storage Insights from The Proper Motion Company.

File uploads seem simple until they are not. A basic implementation – an HTML form, a multipart POST, and a write to disk – takes an afternoon. A production-grade system that handles 500MB videos, resumes interrupted uploads, generates thumbnails, scans for malware, serves files globally with low latency, and does not bankrupt you on storage costs takes weeks of careful engineering.

Most web applications eventually need robust file handling: document management systems, media platforms, e-commerce product images, user profile avatars, insurance claim attachments, construction project photos, medical imaging portals. The requirements differ, but the architectural patterns are remarkably consistent.

This guide covers the full stack of building file upload and storage systems – from the browser-side upload experience through server-side processing, storage architecture, delivery optimization, and cost management.

Client-Side Upload Architecture

The upload experience starts in the browser, and getting it right dramatically reduces support tickets and user frustration.

Chunked uploads for large files. Any file over 10MB should be uploaded in chunks. Chunked uploading breaks a file into segments (typically 5 to 10MB each), uploads them independently, and reassembles them on the server. This provides three critical benefits: progress tracking (you can show a real progress bar instead of a spinner), resumability (if the connection drops after uploading 80 percent of a 200MB file, the user resumes from where they stopped, not from zero), and reliability (retrying a failed 5MB chunk is fast; retrying a failed 200MB upload is painful).

The Tus protocol is an open standard for resumable uploads with client libraries for JavaScript, iOS, and Android, plus server implementations for Node.js, Go, Python, and Ruby. Using Tus instead of building your own chunked upload protocol saves two to three weeks of development and gives you battle-tested resumability.

Direct-to-storage uploads with presigned URLs. For most applications, files should not pass through your application server. Instead, the browser uploads directly to your cloud storage bucket (S3, GCS, or Azure Blob) using a presigned URL. The flow works like this: the client requests an upload URL from your API, your API generates a presigned URL with a short expiration (15 minutes is typical), the client uploads the file directly to storage using that URL, and the client notifies your API that the upload is complete so you can trigger processing.

This pattern removes your application server from the data path entirely. A 500MB file upload does not consume your server’s memory, bandwidth, or CPU. Your API server handles only the metadata – a few kilobytes per upload.

Client-side validation. Validate before uploading to avoid wasting bandwidth and user time. Check file type (by reading the file’s magic bytes, not just the extension – a renamed .exe with a .jpg extension should be caught), file size (enforce limits before the upload starts, not after 20 minutes of uploading), and image dimensions (if your application requires specific aspect ratios or minimum resolutions, check them client-side using the FileReader API and an Image object).

Provide clear, specific error messages: “This file is 250MB but the maximum is 100MB” is actionable. “Upload failed” is not.

Related: The AI Technology Stack: Models, Frameworks, and Infrastructure Guide

Server-Side Processing Pipeline

Once a file lands in storage, a processing pipeline transforms it into the formats your application needs.

Asynchronous processing is mandatory. Never process files synchronously in an API request. The user uploads a file, your API records the upload metadata and returns immediately, and a background job handles all processing. This keeps your API responsive and lets you scale processing workers independently from your web servers.

Use a job queue (Sidekiq, Bull/BullMQ, Celery, or a managed service like AWS SQS) to manage the processing pipeline. Each processing step should be an independent, idempotent job that can be retried on failure.

Image processing. For uploaded images, a typical pipeline generates multiple resized versions: a thumbnail (150x150), a medium display version (800px wide), a large version (1600px wide), and an optimized original. Use sharp (Node.js), Pillow (Python), or ImageMagick for resizing. Convert all output to WebP format (30 to 50 percent smaller than JPEG at equivalent quality) with a JPEG fallback for browsers that do not support WebP.

Strip EXIF metadata from public-facing images. EXIF data can contain GPS coordinates, device serial numbers, and timestamps that users do not intend to share. Preserve EXIF data in the original stored copy if needed for audit purposes.

Document processing. PDFs and office documents need preview generation. Use LibreOffice in headless mode to convert Word, Excel, and PowerPoint files to PDF, then render PDF pages as images for thumbnail previews. For text extraction (enabling search across uploaded documents), Apache Tika or pdf-parse (Node.js) extract text from PDFs, and Tesseract handles OCR for scanned documents.

Video processing. Video is the most resource-intensive file type. Use FFmpeg for transcoding to web-friendly formats (H.264/MP4 for broad compatibility, VP9/WebM for better compression), generating thumbnail frames (extract a frame at the 2-second mark or generate a montage of frames), and creating adaptive bitrate versions (HLS or DASH) for streaming.

Video processing is CPU-intensive and time-consuming. A 10-minute 1080p video takes 2 to 5 minutes to transcode on a modern server. Consider using a dedicated media processing service (AWS MediaConvert, Mux, or Cloudinary) for video rather than running your own FFmpeg workers.

Malware scanning. Any application that accepts user uploads must scan for malware. ClamAV is the standard open-source antivirus scanner. Run it as a service and scan every uploaded file before making it accessible to other users. For higher assurance, use a commercial scanning API (VirusTotal, Scanii, or Metadefender) that checks against multiple antivirus engines.

Quarantine files that fail scanning: move them to a separate storage bucket, notify the user, and alert your security team. Never silently delete uploads – users need to know why their file was rejected.

Storage Architecture and Organization

How you organize files in storage affects performance, cost, and maintainability for the life of the application.

Bucket structure. Use separate buckets (or at minimum, separate prefixes) for different access patterns: a uploads bucket for raw user uploads before processing, a processed bucket for processed versions ready to serve, a private bucket for files that require authentication to access, and a temp bucket for files with automatic expiration (e.g., export files that are valid for 24 hours).

Object key naming. Never use sequential or predictable file paths. A path like /uploads/user/123/document/456.pdf is guessable, which is a security risk even with authentication. Use UUIDs or content-addressed hashing: /uploads/a3f2b8c1-7d4e-4a91-bc3d-9e6f2a1b8c3d/original.pdf. This also eliminates the risk of filename collisions and avoids character encoding issues with user-provided filenames.

Store the original filename in your database metadata, not in the storage path. When a user downloads the file, set the Content-Disposition header to include the original filename.

Lifecycle policies. Configure automatic lifecycle rules to control costs: transition objects from Standard to Infrequent Access storage after 90 days (saves 40 to 50 percent on storage costs for files that are rarely re-accessed), delete temporary uploads that were never completed (chunked uploads that stalled), and expire processed versions that can be regenerated from originals.

For a typical application processing 10,000 file uploads per month with an average size of 5MB, raw storage costs are minimal ($2 to $5/month on S3 Standard). The real cost is in bandwidth (serving files) and processing (transcoding, resizing). Lifecycle policies and CDN caching address these.

Database metadata schema. Your database should track: file UUID, original filename, MIME type, file size in bytes, storage bucket and key, processing status (pending, processing, complete, failed), available variants (thumbnail, medium, large, original), upload timestamp, uploader user ID, and any application-specific metadata (which entity the file belongs to, tags, description).

Index this table on uploader_id, entity_type + entity_id (for “show all files attached to this project” queries), and created_at for chronological listing.

See also: How to Build a Booking and Scheduling System

Delivery and Performance Optimization

Serving files efficiently is as important as storing them correctly.

CDN distribution. Put a CDN (CloudFront, Cloudflare, Fastly) in front of your storage bucket. For public files, configure the CDN to cache indefinitely using immutable cache headers – since your file paths include UUIDs, you can set Cache-Control: public, max-age=31536000, immutable and never worry about cache invalidation. If a file changes, it gets a new UUID and a new path.

Signed URLs for private files. Files that require authentication should be served through signed URLs with short expiration times (5 to 15 minutes). Your API checks the user’s permissions, generates a signed URL, and returns it. The CDN caches the file but the signed URL prevents unauthorized access.

Be careful with signed URL caching. If your CDN caches a signed URL response, subsequent requests with different (or expired) signatures might receive the cached content. Configure your CDN to include the signature in the cache key, or bypass the CDN cache for private files.

Responsive image delivery. For web applications, serve appropriately sized images based on the user’s viewport. Use the srcset attribute in <img> tags to let the browser choose the right size. Your processing pipeline should generate versions at key breakpoints (400w, 800w, 1200w, 1600w), and your API should return all available variant URLs so the frontend can construct proper srcset values.

Download acceleration. For large file downloads (over 50MB), support range requests so browsers can resume interrupted downloads. S3 and GCS handle this natively, but if you proxy downloads through your application server, you must implement range header parsing yourself.

Security Considerations for File Systems

File uploads are one of the most common attack vectors in web applications. Every design decision should account for security.

Content type validation. Never trust the Content-Type header sent by the client. Read the file’s magic bytes (the first few bytes that identify the format) to determine the actual type. The file-type library (Node.js) or python-magic (Python) handle this. Reject files whose magic bytes do not match the claimed content type.

Path traversal prevention. If any part of your system constructs file paths from user input, sanitize rigorously. A filename like ../../../etc/passwd should never result in a read from the filesystem. The simplest prevention is to never use user-supplied filenames in storage paths – use UUIDs exclusively.

Access control enforcement. Every file access must be authorized. Do not rely on obscure URLs as a security mechanism. Even with UUID-based paths, implement proper access control checks before generating signed URLs or serving files. A common mistake is making a storage bucket publicly readable “because the URLs are unguessable” – this fails the moment a URL is logged, shared, or exposed through a browser extension.

Upload size limits at every layer. Enforce size limits at the reverse proxy level (Nginx client_max_body_size), the application level (framework-specific settings), and the storage level (presigned URL conditions). Attackers will try to exhaust your storage quota or processing capacity through oversized uploads.

Building a file system that is fast, reliable, secure, and cost-effective requires deliberate architecture at every layer. But the patterns are well-established, and the investment pays dividends across every feature that touches user-generated content.


Need to build a file upload and storage system that handles real-world scale and complexity? Reach out to The Proper Motion Company. We have built document management, media processing, and file handling systems for applications across industries.

// Related posts