I'm building a photo-sharing app where a photographer uploads an event batch of roughly 3,000–4,000 high-resolution JPEGs, usually 10–15 MB each. Guests will take a selfie and use it to find the event photos they appear in.
I'm deciding whether to resize images in the browser with Web Workers and Canvas before uploading, or upload the originals directly and generate 1080p WebP thumbnails in a backend 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 web server, and I'm considering a queue with separate CPU or GPU workers.
For storage, I'm looking at low-egress object storage such as Cloudflare R2 or Backblaze B2. What architecture would you recommend for uploads, thumbnail generation, embedding extraction, vector search, and serving thumbnails efficiently?
3 Answers
Upload the originals directly to object storage with presigned URLs, then process them asynchronously. Browser-side resizing can save bandwidth, but it puts a lot of work on the photographer’s laptop and makes the upload flow more fragile. A worker using Sharp or libvips can generate thumbnails from the originals and is easier to control and retry. You can still offer an option to discard originals or apply a retention policy if storage cost becomes a concern.
For face recognition, keep it out of the web process. Put uploaded image keys onto a queue and let dedicated workers extract embeddings, detect multiple faces, and write the results to a vector-capable database. CPU processing should be adequate at this scale if jobs can run in the background, while GPU workers are useful when faster turnaround is important. The selfie should pass through the same embedding pipeline before running a cosine-similarity search.
For the vector layer, ArcFace or InsightFace embeddings can be stored in PostgreSQL with pgvector, or in a dedicated engine such as Qdrant or Typesense. At only a few thousand photos per event, PostgreSQL with an appropriate cosine-distance index may be plenty and keeps the system simpler. Remember that each photo can contain several faces, so store one embedding per detected face along with the photo ID and bounding-box metadata. The guest’s selfie also needs face detection and embedding before searching.
R2 is a sensible choice when thumbnail reads could generate significant egress, especially if the files are served through a CDN. B2 can also be cost-effective, but compare download and CDN integration costs for your actual traffic rather than assuming one provider always wins. For a batch of 4,000 images, you probably don’t need an elaborate image platform immediately: object storage, a queue, a small libvips or Sharp worker, and cached thumbnail URLs should be enough. A managed resizing service can reduce maintenance later, but it may be unnecessary at the beginning.
The expected audience matters more than the raw photo count. A few hundred guests viewing a limited gallery may not require aggressive CDN infrastructure, while a public gallery or repeated thumbnail browsing can benefit from caching. Start simple and measure before adding another service.

I wouldn’t automatically resize everything in the browser. It can work in small batches, but users may close the tab, run out of memory, or have inconsistent browser support. Server-side processing gives you predictable output and lets you retain originals when the photographer needs them later.