Blog

Course Creation

How to host video and audio for online courses on Cloudflare R2 (free tier, no surprise bills)

June 9, 2026

If you're building an online course, you'll eventually hit a question that catches most creators off guard: where do I actually put the video files?

YouTube is free but shows competitor recommendations at the end of every video — exactly when your learner just had a breakthrough and was about to do something. Vimeo charges $20/month and caps your upload size. Wistia starts at $24/month. Bunny Stream and Mux are technically excellent but bill by the minute streamed, which can surprise you if your course goes viral.

There's a quieter option that most course creators don't know about: Cloudflare R2. It's storage that's free for the first 10GB, charges almost nothing after that, and — crucially — has no egress fees. This means your learners can stream your videos all day and you don't pay for bandwidth. That's the killer feature.

I host every video and audio file in my course platform on R2. Total monthly cost for a typical solo creator: $0 to $1. I'm going to walk you through exactly how to set it up.

When R2 is the right answer (and when it isn't)

R2 is the right choice when:

  • You have short videos (under about 10 minutes each) embedded in a course or website

  • Your total storage is modest (under 50GB, which is roughly 30-50 polished course videos)

  • You want predictable costs with no metered streaming bills

  • You're embedding videos in your own platform and don't need YouTube-style discovery

  • You don't need automatic transcoding or adaptive bitrate streaming

R2 is not the right choice when:

  • You're building a paid course platform where you need analytics on which videos learners finish (use Cloudflare Stream or Mux instead — they're more expensive but built for this)

  • Your videos are very long (hour-plus modules) where adaptive streaming matters

  • You need built-in DRM or piracy protection

  • You want a turnkey video player with chapters, transcripts, and captions UI

For most solo course creators — especially the short marketing videos, intro modules, and audio narration that make up the bulk of a course — R2 is more than enough. The rest of this post assumes you fit that profile.

The cost story (with real numbers)

Cloudflare R2's pricing as of right now:

  • Storage: $0.015 per GB per month. The first 10GB is free.

  • Class A operations (writes, lists): $4.50 per million. First 1 million free per month.

  • Class B operations (reads): $0.36 per million. First 10 million free per month.

  • Egress: $0. This is the differentiator. Most cloud storage providers charge $0.05-0.09 per GB for outbound bandwidth. R2 charges nothing.

What this means in practice:

A solo course creator with 20 hosted videos averaging 50MB each = 1GB total = $0/month (under the free tier). Even if 10,000 learners watch every video — that's gigabytes of streaming — your bill is still $0 because there's no egress charge.

The same setup on AWS S3 with CloudFront would cost roughly $5-15/month depending on viewer count. On Vimeo Pro: $20/month flat. On Mux at moderate viewing: $30-100/month.

The closest comparison is Backblaze B2 (also no egress through Cloudflare). Both work; R2 wins on simpler setup and tighter integration with Cloudflare's other services if you ever use them.

The catch? R2 is just storage. You don't get a video player, analytics, or transcoding. You're hosting the file and serving it via a public URL. You bring your own player (HTML5 <video> tag is fine for most uses).

Setting up R2: the 15-minute version

This assumes you don't have a Cloudflare account yet. If you do, skip to step 3.

Step 1: Create a Cloudflare account

Go to cloudflare.com and sign up. Free account is fine — you don't need to be on a paid plan to use R2's free tier. You'll need to verify your email.

You don't need to add a website or domain at this stage. R2 works independently.

Step 2: Enable R2

In the Cloudflare dashboard, find R2 in the left sidebar (under "R2 Object Storage" or similar). Click into it.

The first time you visit, Cloudflare will ask you to enable R2. You'll need to add a payment method even though you won't be charged for usage within the free tier. This is Cloudflare's way of preventing abuse — they need to know you're a real person before giving you what is essentially free CDN-grade storage.

Honest note: this is the step that puts some people off. You're giving credit card details for a service you might not get billed for. The free tier limits are generous enough that most solo creators truly never get charged. Cloudflare doesn't auto-upgrade you or charge for anything until you cross the free thresholds.

Step 3: Create your first bucket

Click "Create bucket." A bucket is just a container for your files — think of it like a folder.

Name it something memorable. Examples:

  • mycourse-videos

  • acme-training-media

  • yourname-course-files

Bucket names must be globally unique across all of Cloudflare R2, so pick something distinctive. You can have multiple buckets later if you want to separate different projects.

For location, choose the region closest to your audience. North America/Europe is a safe default if you're global. Australia or Asia-Pacific if your audience is concentrated there. The free tier works the same in any region.

Step 4: Enable public access

This is the step most tutorials miss and most creators get stuck on.

By default, R2 buckets are private. Files you upload can only be accessed with API authentication. For course videos, you want public access — your learners need to be able to load the video URL from a browser.

In your bucket's settings, find Public Access or Public Development URL. Enable it. Cloudflare will give you a URL that looks like:

https://pub-XXXXXXXXXXXXXX.r2.dev/

This is your public bucket URL. Any file you upload to this bucket will be accessible at [public-url]/[filename]. So if you upload intro-video.mp4, it'll be at https://pub-XXX.r2.dev/intro-video.mp4.

For most course creators, the r2.dev URL is fine. It's not the prettiest, but it works and it's permanent. If you want a custom domain (like media.yourcourse.com), I'll cover that briefly in the technical section near the end.

Step 5: You're done with setup

That's it for the account side. You now have a working R2 bucket ready to receive uploads.

Uploading your first file

The simplest way is drag and drop directly in the Cloudflare dashboard:

  1. Open your bucket

  2. Drag a video file into the file list area

  3. Wait for the upload to finish (depends on your file size and connection)

  4. Click the file → copy its public URL

For files larger than a few hundred megabytes, the dashboard can be slow or time out. If that happens, you have two options:

  • Compress the file first (recommended — see the next section)

  • Use a CLI tool like rclone or the AWS CLI (R2 is S3-compatible) — more setup but more reliable for large files

For typical course videos (under 100MB each), the dashboard upload works fine. I've uploaded hundreds of files this way without issues.

Quick file size sanity check: a 90-second talking-head video at 1080p with H.264 compression and 2000 kbps bitrate should be about 3-5MB. If your file is 100MB+ for a short video, your export settings are too high quality — compress it before uploading. Tools like HandBrake (free, open source) can shrink a 200MB raw export to 5MB with no visible quality loss.

Embedding videos in your course or website

The simplest HTML embed:

html

<video
  src="https://pub-XXXXXXXXXXXXXX.r2.dev/your-video.mp4"
  controls
  preload="metadata"
  playsinline
></video>

Three attributes worth knowing:

  • controls — shows the play/pause/volume UI. Without this, learners can't start the video.

  • preload="metadata" — loads just enough of the file to show the duration and first frame, without downloading the whole video on page load. Important for performance.

  • playsinline — critical for iOS Safari. Without it, videos try to go fullscreen automatically on iPhone, which feels broken in a course context.

For autoplaying videos (like a hero animation on a marketing page):

html

<video
  src="https://pub-XXX.r2.dev/hero.mp4"
  autoplay
  muted
  loop
  playsinline
></video>

Note the muted attribute is required for autoplay to work — browsers block autoplay with audio.

For audio files, the equivalent tag is <audio>:

html

<audio
  src="https://pub-XXX.r2.dev/narration.mp3"
  controls
  preload="metadata"
></audio>

If you're using a course platform that supports embed codes or markdown, just paste the R2 URL into the video field. Most platforms (CourseConverter included) will detect the URL and render the player automatically.

The pitfalls I learned the hard way

File format choice

R2 stores anything you give it, but browsers can only play certain formats. For broad compatibility:

  • Video: MP4 with H.264 video and AAC audio. This is the universal format. Avoid older formats like AVI, MOV, or anything with VP9 codec for now — they have spottier mobile support.

  • Audio: MP3 for narration, AAC for higher quality. Both play in every browser without special configuration.

If you're exporting from a video editor, the "Web" or "H.264" preset is what you want. Skip the "ProRes" or "high quality master" presets — those produce huge files that browsers won't play efficiently anyway.

File size discipline

R2's free tier gives you 10GB. That sounds like a lot until you upload a few uncompressed 4K videos. A single 30-minute screen recording at native quality can be 2-3GB.

Two habits that prevent surprises:

  • Compress before upload. HandBrake's "Fast 1080p30" preset is fine for most course content. Set the quality slider to around RF 22-25 — high enough to look good, small enough to load fast.

  • Standardize on 1080p maximum. Course videos rarely need to be higher than 1080p. Higher resolutions waste storage and bandwidth without improving the learning experience.

A standard 90-second talking-head video should land around 3-5MB. A 5-minute screen recording with voice-over should land around 30-50MB. If yours is significantly bigger, your settings need adjusting.

Public vs private access

Files in a public bucket are accessible to anyone with the URL. There's no built-in way to require login or limit access by user.

For most course creators this is fine. Your videos are embedded in your course, the URL isn't promoted publicly, and even if someone shares the link, the access is identical to whether they were enrolled or not.

If you genuinely need access control — for example, a paid course where you don't want non-paying viewers watching the videos — you need signed URLs. That's a more technical setup involving Cloudflare Workers or API authentication. It's possible on R2 but adds complexity. If access control is a hard requirement, consider whether a managed service (Mux, Cloudflare Stream) is worth the cost for the built-in protections.

CORS configuration (only if you hit it)

If your videos load fine when you visit the R2 URL directly but fail when embedded in your course, you're probably hitting a CORS error. By default, R2 allows cross-origin requests, but custom domain setups or strict browser configurations can interfere.

The fix is in your bucket's CORS policy. The basic policy that works for most embedded video use cases:

json

[
  {
    "AllowedOrigins": ["*"],
    "AllowedMethods": ["GET", "HEAD"],
    "AllowedHeaders": ["*"]
  }
]

Paste this in your bucket's CORS settings. It allows any website to load videos from your bucket. If you want to restrict to your own domain, change "*" to your domain. For most course creators, the open policy is fine.

For the technically inclined

A few things you can do if you want to get fancier:

Custom domain. Instead of pub-XXX.r2.dev/video.mp4, you can have media.yourcourse.com/video.mp4. Set up via Cloudflare's DNS (if you have a domain on Cloudflare) — point a subdomain at your R2 bucket. Better looking, less likely to be flagged by paranoid corporate firewalls.

S3-compatible API. R2 supports the S3 API, which means any tool that talks to AWS S3 also talks to R2. Generate API tokens in the Cloudflare dashboard, then use tools like rclone, s3cmd, or AWS SDKs to upload programmatically. Useful for scripts, automated workflows, or hosting files from a CI pipeline.

Signed URLs for time-limited access. If you want videos that work for paying customers but expire after a set time, R2 supports presigned URLs through the S3 API. This is more complex than this post can cover, but it's possible.

Bandwidth analytics. R2's dashboard shows you total bandwidth served, request counts, and storage used. Useful for understanding which videos get watched most. Not as deep as Mux's analytics, but enough for most solo creators.

When to upgrade beyond R2

If you find yourself in any of these situations, it's worth looking at managed video services:

  • You want per-video analytics (completion rates, drop-off points, geographic distribution). Cloudflare Stream is $5/month minimum and gives you all of this.

  • Your videos are getting longer (15+ minutes each), where adaptive bitrate streaming meaningfully improves the viewer experience on slow connections. Mux or Cloudflare Stream both handle this.

  • You're past 50GB of stored video. R2 still works at this scale, but the simpler all-in-one services start looking competitive on time-saved-vs-cost.

  • You need transcoding. R2 doesn't transcode — what you upload is what gets served. If you want one upload to produce multiple resolutions automatically, that's a managed-service feature.

For most solo creators with a course of 20-50 videos averaging a few minutes each, R2 is the right answer. The $0-1 monthly bill is hard to beat.

What to do next

If you've made it through this post, the actual work is small:

  1. Create your Cloudflare account (5 minutes)

  2. Enable R2 and create a bucket (5 minutes)

  3. Enable public access (1 minute)

  4. Upload your first compressed video (a few minutes depending on size)

  5. Copy the URL and paste it into your course

You can do all five steps in 15-20 minutes from a cold start.

Most cloud storage tutorials make this seem more complicated than it is. R2 is genuinely simple. The complexity ramps up only if you need the advanced features (custom domains, signed URLs, API access) — and you can add those incrementally if and when you need them.