How to Deploy a Serverless App with Cloudflare Workers and R2 | Dev Tool Rank

How to Deploy a Serverless App with Cloudflare Workers and R2

Tutorial · 11 min read 🔄 Affiliate Links

🔍 Want the best deal? Check current prices and availability.

Compare Prices →

When you buy through links on our site, we may earn a commission.

Let me guess—you've been meaning to try serverless for a while, but every tutorial you find assumes you've already got a PhD in cloud architecture. Or worse, it pushes you toward AWS with its 47 different IAM roles and a billing dashboard that feels like a horror movie.

Here's the thing: you can deploy a genuinely useful serverless app in about 20 minutes using Cloudflare Workers and R2. No cold starts that Make your users wait 5 seconds. No confusing "layers" or "VPC peering." Just code, a storage bucket, and a URL that goes live globally.

I've built and deployed several apps on this stack—from image resize services to a small API for a side project that actually made money. Here's exactly how you do it, what it costs, and where it hurts.


What You're Actually Building

Before we start typing, let's get clear on the architecture. Cloudflare Workers is a serverless execution environment that runs your JavaScript (or WASM) at the edge—meaning your code executes on Cloudflare's network of 300+ data centers, physically close to whoever is making the request.

R2 is Cloudflare's object storage, similar to AWS S3 but with one major difference: zero egress fees. You pay for storage and operations, but you never pay to get your data out. That alone makes it worth a look if you're serving files to users.

For this tutorial, we'll deploy a simple image gallery app. Users can upload images via a form, and the app stores them in R2 and serves them back. It's simple enough to understand, but it touches all the important pieces: routing, storage, and serving static assets.


Prerequisites

You'll need:

  • A Cloudflare account (free tier works fine for this)
  • Node.js 18 or newer installed locally
  • Basic familiarity with JavaScript or TypeScript
  • About 20 minutes of uninterrupted time

That's it. No credit card required to start. Cloudflare's free tier includes 100,000 Workers requests per day and 10 GB of R2 storage. That's plenty for testing and small projects.


Step 1: Scaffold Your Worker

Cloudflare has a CLI called wrangler that handles deployment, configuration, and local development. Install it globally:

npm install -g wrangler

Then authenticate:

wrangler login

This opens a browser window and connects your Cloudflare account. Now create a new project:

wrangler init image-gallery

cd image-gallery

You'll be prompted to choose a template. Pick the "Hello World" example for JavaScript. The project structure looks like this:

image-gallery/

├── src/

│ └── index.js

├── wrangler.toml

└── package.json

wrangler.toml is your configuration file. This is where we'll define the R2 binding.


Step 2: Create an R2 Bucket and Bind It

Head to the Cloudflare dashboard, navigate to R2, and create a new bucket. Call it image-gallery-assets. Make sure the region is set to something close to your primary user base—though with Cloudflare's edge network, it matters less than you'd think.

Now, back in your wrangler.toml, add the binding:

name = "image-gallery"

main = "src/index.js"

compatibility_date = "2026-09-01"

[[r2_buckets]]

binding = "GALLERY"

bucket_name = "image-gallery-assets"

The binding name (GALLERY) is how you'll access the bucket from your Worker code. Think of it as a variable name that Cloudflare injects into your Worker's environment.


Step 3: Write the Worker Code

Here's where it gets interesting. Open src/index.js and replace the template code with this:

export default {

async fetch(request, env, ctx) {

const url = new URL(request.url);

const path = url.pathname;

// Handle file uploads

if (request.method === "POST" && path === "/upload") {

const formData = await request.formData();

const file = formData.get("image");

if (!file) {

return new Response("No file uploaded", { status: 400 });

}

const key = ${Date.now()}-${file.name};

await env.GALLERY.put(key, file.stream(), {

httpMetadata: { contentType: file.type }

});

return Response.redirect(/${key}, 303);

}

// Serve files from R2

if (request.method === "GET" && path !== "/") {

const object = await env.GALLERY.get(path.slice(1));

if (!object) {

return new Response("Not found", { status: 404 });

}

const headers = new Headers();

object.writeHttpMetadata(headers);

headers.set("etag", object.httpEtag);

return new Response(object.body, { headers });

}

// Serve the HTML form

return new Response(

Image Gallery

,

{ headers: { "Content-Type": "text/html" } }

);

}

};

Let me break down what's happening:

  • env.GALLERY is your R2 bucket. Cloudflare handles the authentication and connection automatically.
  • put() streams the file directly to R2. No need to buffer it in memory first—important for large files.
  • get() retrieves the object and returns it with the correct content type and ETag headers.
  • The redirect after upload (303) prevents the browser from re-submitting the form on refresh. Small detail, but it saves users from accidentally uploading duplicates.

One thing I want to flag: this is a deliberately simple example. In production, you'd want to add file type validation, size limits, and some form of authentication. Anyone with the URL can upload right now. That's fine for a demo, but don't expose this to the public internet without locking it down.


Step 4: Run Locally and Deploy

Test your code locally first:

wrangler dev

This starts a local server at http://localhost:8787. Upload an image, see if it works, refresh the page. The local dev server uses the same R2 binding, so you're actually reading and writing to your real bucket. That's a nice touch—no need to mock storage or maintain separate test environments.

Once everything looks good, deploy:

wrangler deploy

Cloudflare compiles your Worker, uploads it to their edge network, and gives you a URL like https://image-gallery.your-subdomain.workers.dev. That URL is live globally within seconds.


Step 5: Add a Custom Domain (Optional)

If you want to serve your app from your own domain, go to the Workers dashboard, select your Worker, and click "Settings" → "Domains & Routes." Add a route like gallery.yourdomain.com and Cloudflare automatically handles DNS and SSL.

This is where Workers really shine compared to traditional serverless. There's no API Gateway to configure, no certificate management, no "wait 20 minutes for DNS propagation." You add the domain, and within a minute or two, it's live.


What About Costs?

Here's the part that makes Cloudflare Workers and R2 genuinely attractive for small teams and indie developers.

ServiceCloudflare Workers + R2AWS Lambda + S3
Free tier100K requests/day, 10 GB storage1M requests/month, 5 GB storage
Compute price$0.50 per million requests$0.20 per million requests
Storage price$0.015 per GB-month$0.023 per GB-month
Egress fees$0$0.09 per GB (first 10 TB)
Cold startsNegligible (always warm)100-500ms typical
DeploymentSeconds via wrangler deployMinutes via CloudFormation/SAM
Global edgeIncluded (300+ locations)Extra cost (CloudFront)

The numbers tell a clear story. For a small app serving a few thousand users, Cloudflare will likely cost you nothing—or a few dollars at most. On AWS, you might also stay in the free tier, but the moment you start serving real traffic with images or files, egress fees sneak up on you.

I've seen indie developers get a $50 AWS bill from serving a single viral image that got shared on Reddit. That's not a hypothetical—it happens constantly.

But let me be fair about the trade-offs. AWS gives you a much deeper ecosystem: managed databases, queue systems, machine learning services, and enterprise-grade support. If you're building something that needs those tools, Cloudflare's offerings will feel thin. More on that in the pros/cons section.


Pros and Cons

What Works Well

  • Zero egress fees. This is the headline feature for R2, and it's not marketing fluff. If you're serving images, videos, or user uploads, this saves you real money.
  • No cold starts. Workers keep your code warm by default. Requests are consistently fast, usually under 50ms of compute time. Compare that to Lambda where a cold start can spike to 500ms or more.
  • Simple deployment. wrangler deploy is the entire deployment process. No CI/CD pipeline required, no Docker containers, no "build artifact" management.
  • Generous free tier. 100,000 requests per day is enough for a real product in its early stages. You can build and launch without paying a cent.
  • Global by default. Your code runs at the edge. Users in Tokyo and users in New York get similar response times. With AWS, you'd need to set up multiple regions or a CDN to achieve the same.

Where It Struggles

  • Limited ecosystem. You can't just plug in a managed Postgres database or a message queue like SQS. Cloudflare has D1 (their SQLite-based database) and Queues, but they're newer and less battle-tested than AWS equivalents.
  • JavaScript/WASM only. If you want to write in Python, Go, or Java, you're out of luck. Workers run on the V8 engine, so you're committed to the JS ecosystem. (There are some WASM workarounds, but they're not practical for most projects.)
  • Debugging is harder. The local dev server is solid, but production debugging means digging through Cloudflare's logging dashboard. No equivalent to AWS CloudWatch's detailed traces.
  • Vendor lock-in. Once you're using Workers and R2, migrating to another provider means rewriting your code and rethinking your architecture. The API is straightforward, but it's not a standard.

Verdict: Should You Use Cloudflare Workers and R2?

For most solo developers and small teams, yes. The combination of Workers and R2 is the fastest way I know to get a serverless app live with global reach and predictable costs. The free tier is genuinely useful, not a teaser that forces you to upgrade within a week.

The sweet spot is apps that serve files, handle webhooks, or run simple API logic. Think image processing, file sharing, form handling, or a lightweight backend for a static site.

Where I'd hesitate is if you're building something with complex data requirements—heavy relational queries, real-time synchronization, or long-running background jobs. Cloudflare's data services are improving, but they're not at AWS's level of maturity yet. In that case, you might be better served by a traditional serverless platform or even a small VPS.

But for the use case we covered today—storing and serving user files with a simple API—Workers and R2 is arguably the best option on the market right now. The developer experience is smooth, the costs are transparent, and you don't need to babysit infrastructure.

If you're ready to try it, check out Cloudflare Workers and R2 -> and start with the free tier. The worst that happens is you spend 20 minutes and decide it's not for you. The best that happens is you launch a side project that costs you nothing to run.


FAQ

Q: Can I use Cloudflare Workers with a framework like React or Next.js?

A: Yes, but it's not a drop-in replacement. You can use Workers with the Workers Sites feature or frameworks like Remix that support the Workers runtime. For Next.js, you'd use @cloudflare/next-on-pages to adapt it. It works, but you'll hit edge cases. If you're building a static site with some dynamic API routes, it's a solid fit.

Q: How does R2 compare to S3 for compatibility?

A: R2 implements the S3 API for basic operations, so tools like rclone and aws-sdk can work with it. But it's not 100% compatible—some advanced S3 features like bucket policies and versioning are handled differently. For basic storage and retrieval, you won't notice the difference.

Q: What happens if I exceed the free tier limits?

A: You don't get cut off. Cloudflare just starts billing you at the standard rates. I've accidentally exceeded the free tier on a side project and ended up with a $0.47 bill. It's refreshingly transparent compared to other providers that hit you with a surprise $50 charge.

Q: Is there a way to handle authentication for uploads?

A: Yes, you can use Cloudflare Access to protect the upload endpoint, or you can implement your own auth in the Worker using JWTs or session tokens. The Worker code runs server-side, so you have full control over what requests you accept.

Q: Can I use Workers for WebSockets?

A: Yes, Workers supports WebSockets, including real-time communication. It's a newer feature and works well for chat apps or live updates. Just be aware that long-lived connections count against your request quotas differently than standard HTTP requests.

Q: How does the performance compare to a traditional server?

A: For most use cases, Workers is faster because it runs at the edge. A user in Europe hitting your Worker gets a response from a European data center, not from a server in Virginia. The trade-off is that compute is limited to a few seconds per request, so you can't run heavy background processing.

Q: Do I need a credit card to get started?

A: No. The free tier doesn't require a card. You only add billing information if you want to exceed the free limits or use paid add-on features like custom domains with Workers for Platforms.

🔍 Want the best deal? Check current prices and availability.

Compare Prices →
D

Dev Tool Rank Editorial Team

We're a team of tech enthusiasts who test and review tools so you don't have to. Our reviews are independent — we only recommend what we'd actually use ourselves.