How simplerbook blew past its free tier bandwidth
This month, simplerbook — my appointment and event booking tool for small businesses — exceeded its Convex free tier bandwidth. The product has been slowly growing, both in traffic and users, and that growth combined with how I was storing images finally caught up with me.
What went wrong
Simplerbook lets business owners upload logos, banners, gallery images, and event covers. When I first built the upload flow, I kept it simple: take the file, store it in Convex file storage, serve it back. No compression, no resizing, no format conversion. A 4MB phone photo went in as a 4MB phone photo.
The thing is, Convex file storage isn’t really designed for serving production images at scale — their own docs suggest using a dedicated storage service for that. But it worked, so I shipped it. Every page load pulled those full-size originals straight from Convex, and as more users signed up and more pages got visited, the bandwidth quietly added up until it didn’t.
The fix: preprocess before you store
The first change was making sure images are as small as they need to be before they ever hit storage. Now, when a user uploads an image, it goes through client-side processing:
- Cropped to the correct aspect ratio for its field — 1:1 for logos, 4:1 for banners, roughly 1.9:1 for event covers
- Resized to sensible max dimensions — 400x400 for a logo, 1600x400 for a banner, 1200x1200 for gallery images
- Compressed to WebP at 0.8 quality
A 4MB JPEG becomes a 50-100KB WebP. There’s also a hard 2MB cap enforced both in the browser and on the server, just in case.
The fix: move to Cloudflare R2
The second change was migrating image storage from Convex to Cloudflare R2. R2 is purpose-built for object storage and doesn’t charge for egress — which is exactly the cost that bit me on Convex.
This wasn’t a “delete the old stuff and start fresh” migration. Existing users had images that needed to keep working, so I wrote a migration that moved every stored image to R2 under a new owner-scoped key structure, verified the data, and only then removed the legacy resolution code. No data lost.
Images are now served through images.simplerbook.com, and since every filename contains a UUID — meaning the content at a given URL never changes — I set up Cloudflare caching rules to cache everything for a year. That means most image requests never even reach R2. Between the preprocessing and the aggressive caching, the bandwidth profile is completely different now.
The takeaway
Two lessons, both obvious in hindsight: preprocess your uploads before storing them, and don’t lean on your app backend as a CDN. Convex is great for what it’s built for — reactive queries, serverless functions, real-time sync — but serving raw image blobs at scale isn’t that.
This post was written by Claude.