I'm building an ecommerce chatbot that helps visitors find products in inventory. Users may upload photos of items they're looking for, and the chatbot responds with product suggestions. I plan to store those customer images in a Cloudflare R2 bucket, but I'm unsure whether to serve them through caching or use direct R2 URLs after upload. Each image will probably only be viewed a few times. A cache could reduce storage reads, but cached files might remain accessible to anyone with the URL until they're removed. Alternatively, I could keep the bucket private and generate presigned URLs that expire after a short period. What's the better design for this use case, and should I consider another storage provider?
4 Answers
Caching is probably unnecessary if each image is only viewed once or twice. Keep the objects private and use signed URLs with a short expiration time.
The security difference isn’t quite what it seems: a presigned URL is still a bearer link that anyone can use while it’s valid, just like a cached public URL. Expiration helps, but it doesn’t prevent forwarding during that window.
Keep the bucket private and treat these uploads as part of the chat data rather than public assets. For uploads, issue a short-lived write URL, accept the file, and save only the resulting object key. When displaying the image, authorize access to the chat first and mint a temporary read URL. Add a lifecycle rule for the relevant bucket or prefix so abandoned images are automatically deleted after a reasonable period; that cleanup is often more important than caching.
A good pattern is to store the R2 object key with the chat message, verify that the requester is allowed to view that conversation, and then generate a fresh, short-lived GET URL when the chat history loads. Don’t save the signed URL itself because it will eventually expire and produce broken images. A few minutes is safer than several days for customer photos. Keep signed links out of logs and analytics, define a retention policy, and use bucket lifecycle rules to automatically delete abandoned uploads. If you need immediate revocation or authorization on every request, serve the image through an authenticated application endpoint instead. With such low access volume, measure R2 usage before introducing a cache.
That sounds like the best approach. Generating a fresh URL when the conversation is opened also avoids storing links that later stop working.

Good point about lifecycle rules. Automatically removing abandoned uploads is something I hadn’t considered.