Blog Post

How I Built a Serverless File-Sharing Tool with Cloudflare R2

Building a file-sharing application usually comes with a massive hidden catch: bandwidth costs. If you build a tool that goes viral and users download a 1GB video file 10,000 times from AWS S3, you are going to wake up to a devastating egress bill. Traditional cloud providers charge a premium for letting data leave their network.

But the rules of cloud storage have changed. Cloudflare R2 is an S3-compatible object storage service with one massive selling point: zero egress fees. In this technical case study, I will break down exactly how I built a highly secure, serverless file-sharing tool utilizing Cloudflare Workers and R2.

The Problem with Traditional File Uploads

In a standard Node.js/Express stack, a file upload works like this:

  1. The user uploads the file to your web server.
  2. Your server holds the file in memory or temporary storage.
  3. Your server then uploads that file to an S3 bucket.

This "middleman" approach is terrible for performance and ruins server scalability. If ten users upload 500MB files simultaneously, your server's memory will instantly crash. We need to eliminate the middleman.

The Serverless Architecture: Workers + R2

To make this app blazingly fast and infinitely scalable, we remove the origin server completely. We use a Cloudflare Worker to act as the authentication layer, but the actual file data streams directly into the R2 Bucket.

Step 1: Generating Presigned URLs

Instead of the user sending the file to our Worker, the Worker generates a temporary, secure "ticket" that allows the user's browser to upload directly to R2. This is called a Presigned URL.

// Inside our Cloudflare Worker
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";

const S3 = new S3Client({
  region: "auto",
  endpoint: `https://${ACCOUNT_ID}.r2.cloudflarestorage.com`,
  credentials: {
    accessKeyId: ACCESS_KEY,
    secretAccessKey: SECRET_KEY,
  },
});

export async function handleUploadRequest(request) {
  const fileName = crypto.randomUUID() + ".pdf";
  const command = new PutObjectCommand({ Bucket: "my-share-bucket", Key: fileName });
  
  // Generate a URL valid for exactly 15 minutes
  const signedUrl = await getSignedUrl(S3, command, { expiresIn: 900 });
  
  return Response.json({ uploadUrl: signedUrl, fileId: fileName });
}

Step 2: The Direct Browser Upload

On the frontend, the user selects a file. We fetch the `uploadUrl` from our Worker, and then perform a standard `PUT` request directly to that URL. The data bypasses our compute layer entirely, meaning we don't pay for Worker execution time during massive uploads!

Downloading Files Securely

Because we don't want our R2 bucket to be public (which would allow anyone to hotlink our files), we use the Worker to securely stream the download back to the user.

Thanks to the native R2 bindings in Cloudflare Workers, retrieving a file is astonishingly simple and fast:

// Serving the file download
export async function handleDownload(request, env) {
  const fileId = new URL(request.url).searchParams.get("id");
  
  // Fetch object from R2 binding
  const file = await env.MY_BUCKET.get(fileId);
  
  if (file === null) {
    return new Response('File not found or expired', { status: 404 });
  }

  const headers = new Headers();
  file.writeHttpMetadata(headers);
  headers.set('etag', file.httpEtag);
  headers.set('Content-Disposition', `attachment; filename="${fileId}"`);

  // Stream the file directly to the user
  return new Response(file.body, { headers });
}

Handling File Expiration (Auto-Delete)

A true file-sharing service needs files to expire. Luckily, R2 supports Object Lifecycles. Instead of writing a cron job to delete old files, you simply configure the R2 bucket policy to automatically delete any object 24 hours after it was created. It requires zero code and guarantees your storage costs never spiral out of control.

Why Cloudflare R2 is the Ultimate S3 Alternative

By shifting from AWS S3 to Cloudflare R2, this application unlocks three massive benefits:

  • Zero Egress Fees: Whether a file is downloaded 10 times or 10 million times, I am not charged a single cent for outbound data transfer.
  • Global Edge Performance: Cloudflare automatically routes requests to the nearest data center, meaning uploads and downloads are incredibly fast globally.
  • Native Worker Integration: Using the `env.BUCKET.get()` binding is significantly faster and cleaner than writing standard S3 SDK API calls.

If you are building an application in 2026 that handles user-generated media, assets, or heavy downloads, combining Workers and R2 is hands-down the most cost-effective architecture available.