Integrating R2 with Workers: Serving Files at the Edge
Learn how to bind an R2 bucket to your Cloudflare Worker, fetch files programmatically, and handle errors to build a resilient, edge-based file storage system.

Previously in this course, we explored Introduction to R2 Storage: Buckets and Object Storage to understand object storage concepts, and in Uploading Files to R2: A Practical Guide for Developers, we mastered manual file uploads via the CLI. In this lesson, we will bridge the gap by connecting your storage directly to your compute layer.
By binding an R2 bucket to a Worker, you gain the ability to manipulate, serve, and secure your files dynamically at the edge without a traditional origin server.
Binding R2 to a Worker
In the Cloudflare ecosystem, a "binding" is a bridge that allows your code to interact with external services (like R2, D1, or KV) using a variable name. Without this binding, your Worker script cannot "see" your bucket.
To bind your bucket, you must modify your wrangler.toml file. This tells the Cloudflare infrastructure which bucket to inject into your Worker’s execution context.
Open your wrangler.toml and add the following block:
TOML[[r2_buckets]] binding = 'MY_BUCKET' # The variable name in your code bucket_name = 'my-app-assets' # The name of your R2 bucket
After saving this change, run npx wrangler deploy to update your environment. Your Worker can now access the R2 API via the env.MY_BUCKET object.
Serving an Image from R2

Once the binding is active, retrieving a file is straightforward. You use the get() method provided by the R2 bucket API. Let’s create a simple Worker that serves an image from your bucket based on the request URL.
Replace your index.js (or src/index.ts) content with this:
JAVASCRIPTexport default { async fetch(request, env) { const url = new URL(request.url); const key = url.pathname.slice(1); // Remove leading slash // 1. Fetch the object from R2 const object = await env.MY_BUCKET.get(key); // 2. Handle file retrieval errors if (object === null) { return new Response(CE9178">'Object Not Found', { status: 404 }); } // 3. Serve the file with appropriate headers const headers = new Headers(); object.writeHttpMetadata(headers); headers.set(CE9178">'etag', object.httpEtag); return new Response(object.body, { headers, }); }, };
Understanding the Mechanics
env.MY_BUCKET.get(key): This performs an asynchronous fetch from your storage.object.writeHttpMetadata(headers): This is a powerful helper method. It automatically copies metadata (likeContent-Type) you might have set during the upload process to your response, ensuring the browser renders the image correctly.object.body: This is a readable stream, which means your Worker doesn't load the entire file into memory at once. It streams the data directly from R2 to the end user.
Hands-on Exercise: Implementing an Error Fallback
Your task is to improve the error handling in the code above. Currently, if an image is missing, you return a 404. Modify your Worker to return a default image (e.g., placeholder.png) if the requested key does not exist in the bucket.
- Create a
placeholder.pngand upload it to your R2 bucket. - Update your
fetchfunction logic: ifobject === null, attempt to fetchplaceholder.pnginstead. - If
placeholder.pngis also missing, return a custom 404 response.
Common Pitfalls
- Case Sensitivity: R2 keys are case-sensitive. If your file is named
Logo.pngand your URL requestslogo.png, theget()method will returnnull. - Memory Limits: While streaming (
object.body) prevents memory exhaustion, avoid trying to useawait object.text()orawait object.arrayBuffer()on large files. Always stream large assets directly to the response. - Binding Mismatches: If your
wrangler.tomlbinding name doesn't match the variable used in your code (e.g., usingenv.ASSETSwhile the config saysMY_BUCKET), the code will throw an undefined error at runtime.
FAQ
Can I list files in my bucket from a Worker?
Yes, you can use env.MY_BUCKET.list(). This is useful for building dynamic galleries or index pages.
Does serving files from R2 through a Worker count as egress? Cloudflare R2 has no egress fees. Serving files via a Worker to the public internet is free of egress charges, regardless of the volume of data.
How do I handle private files?
By using a Worker, you can add authentication checks before calling env.MY_BUCKET.get(). This is the standard way to serve private content.
Recap

In this lesson, we successfully integrated R2 storage into our application by configuring bindings in wrangler.toml and utilizing the R2 API to serve files. We also learned how to leverage streaming for performance and handle missing assets gracefully. This forms the foundation for the file-serving backend of our project.
Up next: Introduction to D1 SQL Database, where we will add a relational database layer to our application to store file metadata and user information.
Work with me

Custom Email & File Storage System on Cloudflare (Google Workspace Alternative)
Your own private email + file storage suite on your domain — unlimited mailboxes, no per-seat fees. A self-owned Google Workspace alternative for a flat ~$5/month.

Laravel SaaS MVP & Multi-Tenant App Development
Launch your SaaS MVP on Laravel — multi-tenant, subscription-ready, and built by the engineer behind a platform serving 10,000+ paying users.


