I'm building a photo-sharing app where photographers upload event batches of roughly 3,000–4,000 high-resolution JPEGs, usually 10–15 MB each. Guests can submit a selfie and retrieve the event photos they appear in.
I'm trying to decide whether image resizing should happen in the browser with Web Workers and Canvas, producing smaller WebP files before upload, or on the backend with a queue worker using tools such as Sharp, libvips, or Pillow. I also need a reliable way to extract 128-dimensional face embeddings with ArcFace or InsightFace without overwhelming the main application server. Finally, I'm comparing low-egress object storage options such as Cloudflare R2 and Backblaze B2 for storing originals and serving thumbnails.
3 Answers
Upload the originals directly to object storage using presigned URLs, then enqueue each object key for background processing. A worker using Sharp or libvips can generate thumbnails without tying up the web server. For embeddings, use a separate queue and worker pool, then store the vectors in PostgreSQL with pgvector or a dedicated vector database. The selfie needs to go through the same embedding pipeline before running a cosine-similarity search. R2 is a reasonable choice when thumbnail reads may be served frequently because its lack of egress fees can simplify the cost model, especially when paired with caching.
For a simpler setup, a managed image-resizing service can remove the need to operate your own libvips or imgproxy workers. It is worth comparing its per-image and storage costs against running a small background service.
The architecture does not need to be overly complicated for only a few thousand images. A basic object-storage bucket, one thumbnail worker, and one embedding worker may be enough. A CDN is helpful if the same thumbnails are repeatedly viewed, but it is not mandatory for a single event with a few hundred guests. Libvips is a particularly efficient choice for local thumbnail generation.
Keep the image-processing pipeline asynchronous: record the upload, push a job containing the object key, and let workers process images in controlled batches. Limit concurrency based on available CPU and memory rather than launching thousands of tasks at once. ArcFace-style models can run on CPU for this volume if processing is allowed to continue in the background, while GPU workers are useful when results need to be available quickly. Store the resulting embeddings in pgvector, Qdrant, or Typesense and add retries plus job status tracking.

Browser-side resizing can reduce upload volume and server work if the uploader’s laptop is reasonably capable. Processing a small number of images concurrently is safer than trying to resize the entire batch at once. You could also discard or downsize originals if the product does not actually need full-resolution files.