Mux vs Cloudflare Stream vs Bunny Stream: Video CDN 2026
Mux vs Cloudflare Stream vs Bunny Stream: Video CDN 2026
TL;DR
Hosting video yourself means encoding pipelines, CDN distribution, adaptive bitrate streaming, and player maintenance. Video hosting APIs abstract this into a simple upload-and-stream workflow. Mux is the developer-first video infrastructure platform — upload a video, get HLS adaptive bitrate streams, thumbnails, GIF previews, captions, and per-second analytics; designed for SaaS products embedding video. Cloudflare Stream is the simplest and often cheapest option — deeply integrated into the Cloudflare network, streams through the same CDN as your other Cloudflare assets, and has a minimal but sufficient API. Bunny Stream is the most affordable at scale — European-based CDN with competitive per-GB pricing, video processing, and a built-in player with DRM options. For developer-first video products with analytics: Mux. For Cloudflare users wanting the simplest integration: Cloudflare Stream. For cost-sensitive applications with high video volume: Bunny Stream.
Key Takeaways
- Mux pricing: $0.015/minute stored + $0.045/1GB delivered — per-minute storage model
- Cloudflare Stream: $5/1,000 minutes stored + $1/1,000 minutes viewed — per-view model
- Bunny Stream: $0.0055/GB CDN + $0.02/minute encoding — lowest cost at high volume
- Mux Data — per-second video quality analytics (rebuffering, startup time, quality score)
- Cloudflare Stream uses MP4-to-HLS — automatic adaptive bitrate from any upload
- Bunny Stream supports DRM — Widevine + FairPlay for content protection
- All three support direct browser upload — no server required for file upload
Use Cases
SaaS with video + analytics (like Loom, Wistia) → Mux (Mux Data analytics)
Already on Cloudflare (CDN, Workers, Pages) → Cloudflare Stream
High-volume VOD on a budget → Bunny Stream
DRM content protection → Bunny Stream or Mux
Low-latency live streaming → Mux (sub-3s RTMP)
Simple video embeds with no analytics → Cloudflare Stream
European data residency → Bunny Stream (EU-based)
Mux: Developer-First Video Infrastructure
Mux is the video platform designed for SaaS developers — upload via URL or direct, get playback URLs, thumbnails, GIF previews, captions, and detailed viewer analytics.
Installation
npm install @mux/mux-node # Server SDK
npm install @mux/mux-player-react # Video player component
Upload Video from URL
import Mux from "@mux/mux-node";
const mux = new Mux({
tokenId: process.env.MUX_TOKEN_ID!,
tokenSecret: process.env.MUX_TOKEN_SECRET!,
});
// Create an asset from a URL (Mux downloads and processes it)
async function uploadVideoFromUrl(videoUrl: string): Promise<string> {
const asset = await mux.video.assets.create({
input: [{ url: videoUrl }],
playback_policy: ["public"],
mp4_support: "capped-1080p", // Also generate MP4 for download
encoding_tier: "baseline", // "baseline" | "smart"
video_quality: "plus",
master_access: "none",
});
console.log("Asset ID:", asset.id);
console.log("Playback ID:", asset.playback_ids?.[0]?.id);
return asset.id;
}
// Poll for asset status (or use webhooks)
async function waitForAssetReady(assetId: string): Promise<Mux.Video.Asset> {
let asset = await mux.video.assets.retrieve(assetId);
while (asset.status === "preparing") {
await new Promise((resolve) => setTimeout(resolve, 2000));
asset = await mux.video.assets.retrieve(assetId);
}
return asset;
}
Direct Browser Upload
// Server: Create an upload URL
// app/api/mux/upload/route.ts
export async function POST() {
const upload = await mux.video.uploads.create({
new_asset_settings: {
playback_policy: ["public"],
encoding_tier: "baseline",
},
cors_origin: process.env.NEXT_PUBLIC_APP_URL!,
});
return Response.json({
uploadId: upload.id,
uploadUrl: upload.url, // PUT video bytes directly to this URL
});
}
// Client: Upload file directly to Mux
import * as UpChunk from "@mux/upchunk";
async function uploadVideo(file: File) {
const { uploadUrl } = await fetch("/api/mux/upload", { method: "POST" }).then((r) => r.json());
const upload = UpChunk.createUpload({
endpoint: uploadUrl,
file,
chunkSize: 30720, // 30MB chunks
});
upload.on("progress", (progress) => {
setUploadProgress(progress.detail);
});
upload.on("success", () => {
console.log("Upload complete!");
});
upload.on("error", (error) => {
console.error("Upload error:", error.detail);
});
}
Mux Player (React)
import MuxPlayer from "@mux/mux-player-react";
function VideoPlayer({ playbackId }: { playbackId: string }) {
return (
<MuxPlayer
playbackId={playbackId}
metadata={{
video_title: "My Video",
viewer_user_id: "user-123", // For Mux Data viewer analytics
}}
streamType="on-demand"
autoPlay={false}
muted={false}
poster={`https://image.mux.com/${playbackId}/thumbnail.jpg?time=10`}
style={{ width: "100%", aspectRatio: "16/9" }}
/>
);
}
// Thumbnail URL patterns
// https://image.mux.com/{PLAYBACK_ID}/thumbnail.jpg?time=10 (still at 10s)
// https://image.mux.com/{PLAYBACK_ID}/animated.gif?start=10&end=15 (animated GIF)
// https://image.mux.com/{PLAYBACK_ID}/storyboard.vtt (seeking preview sprites)
Webhooks
// app/api/mux/webhook/route.ts
import { headers } from "next/headers";
import Mux from "@mux/mux-node";
const mux = new Mux({ tokenId: process.env.MUX_TOKEN_ID!, tokenSecret: process.env.MUX_TOKEN_SECRET! });
export async function POST(req: Request) {
const body = await req.text();
const headersList = await headers();
const signature = headersList.get("Mux-Signature") ?? "";
// Verify webhook signature
mux.webhooks.verifySignature(body, { "mux-signature": signature }, process.env.MUX_WEBHOOK_SECRET!);
const event = JSON.parse(body);
switch (event.type) {
case "video.asset.ready": {
const asset = event.data;
await db.video.update({
where: { muxAssetId: asset.id },
data: {
status: "ready",
playbackId: asset.playback_ids?.[0]?.id,
duration: asset.duration,
},
});
break;
}
case "video.asset.errored": {
await db.video.update({
where: { muxAssetId: event.data.id },
data: { status: "error" },
});
break;
}
}
return Response.json({ received: true });
}
Signed Playback (Private Videos)
// Create signed playback token for private assets
import jwt from "jsonwebtoken";
function createSignedPlaybackToken(playbackId: string, keyId: string, privateKey: string): string {
const now = Math.floor(Date.now() / 1000);
return jwt.sign(
{
sub: playbackId,
aud: "v", // "v" = video, "t" = thumbnail, "g" = gif, "s" = storyboard
exp: now + 3600, // 1 hour expiry
kid: keyId,
},
privateKey,
{ algorithm: "RS256" }
);
}
// Use in player
const token = createSignedPlaybackToken(playbackId, keyId, privateKey);
// https://stream.mux.com/{PLAYBACK_ID}.m3u8?token={TOKEN}
Cloudflare Stream: Simplest Video API
Cloudflare Stream is the lowest-friction video API — especially for teams already on Cloudflare. Upload a video, embed it, done.
Direct Upload from Browser
// Server: Get a one-time upload URL
async function createStreamUploadUrl(): Promise<{ uploadUrl: string; uid: string }> {
const response = await fetch(
`https://api.cloudflare.com/client/v4/accounts/${process.env.CF_ACCOUNT_ID}/stream/direct_upload`,
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.CF_API_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
maxDurationSeconds: 3600,
requireSignedURLs: false,
meta: { name: "User Upload" },
}),
}
);
const data = await response.json();
return { uploadUrl: data.result.uploadURL, uid: data.result.uid };
}
// Client: Upload directly to Cloudflare
async function uploadToCloudflare(file: File) {
const { uploadUrl, uid } = await fetch("/api/stream/upload", { method: "POST" }).then((r) => r.json());
// Direct TUS upload
const upload = new tus.Upload(file, {
endpoint: uploadUrl,
chunkSize: 50 * 1024 * 1024, // 50MB chunks
onProgress: (uploaded, total) => {
setProgress(Math.round((uploaded / total) * 100));
},
onSuccess: () => {
console.log("Upload complete! Video UID:", uid);
// Poll: https://api.cloudflare.com/.../{uid} until readyToStream: true
},
});
upload.start();
}
Embed Player
// Cloudflare Stream iframe embed
function CloudflarePlayer({ videoId }: { videoId: string }) {
return (
<div style={{ position: "relative", paddingTop: "56.25%" }}>
<iframe
src={`https://customer-${process.env.NEXT_PUBLIC_CF_CUSTOMER_CODE}.cloudflarestream.com/${videoId}/iframe`}
allow="accelerometer; gyroscope; autoplay; encrypted-media; picture-in-picture;"
allowFullScreen
style={{ border: "none", position: "absolute", top: 0, left: 0, width: "100%", height: "100%" }}
/>
</div>
);
}
List and Manage Videos
async function listVideos(accountId: string, apiToken: string) {
const response = await fetch(
`https://api.cloudflare.com/client/v4/accounts/${accountId}/stream`,
{ headers: { Authorization: `Bearer ${apiToken}` } }
);
const data = await response.json();
return data.result; // Array of video objects
}
async function deleteVideo(videoId: string) {
await fetch(
`https://api.cloudflare.com/client/v4/accounts/${process.env.CF_ACCOUNT_ID}/stream/${videoId}`,
{
method: "DELETE",
headers: { Authorization: `Bearer ${process.env.CF_API_TOKEN}` },
}
);
}
Bunny Stream: Cost-Efficient Video CDN
Bunny Stream offers competitive pricing with global CDN, video processing, and optional DRM — popular for high-volume content delivery.
Upload and Manage via REST API
const BUNNY_API_KEY = process.env.BUNNY_API_KEY!;
const BUNNY_LIBRARY_ID = process.env.BUNNY_LIBRARY_ID!;
// Create a video entry
async function createBunnyVideo(title: string): Promise<{ videoId: string }> {
const response = await fetch(
`https://video.bunnycdn.com/library/${BUNNY_LIBRARY_ID}/videos`,
{
method: "POST",
headers: {
AccessKey: BUNNY_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({ title }),
}
);
const data = await response.json();
return { videoId: data.guid };
}
// Upload video bytes
async function uploadBunnyVideo(videoId: string, videoBuffer: Buffer): Promise<void> {
await fetch(
`https://video.bunnycdn.com/library/${BUNNY_LIBRARY_ID}/videos/${videoId}`,
{
method: "PUT",
headers: {
AccessKey: BUNNY_API_KEY,
"Content-Type": "application/octet-stream",
},
body: videoBuffer,
}
);
}
// Full upload pipeline
async function uploadVideo(title: string, videoPath: string): Promise<string> {
const { videoId } = await createBunnyVideo(title);
const videoBuffer = fs.readFileSync(videoPath);
await uploadBunnyVideo(videoId, videoBuffer);
// Playback URL: https://[hostname].b-cdn.net/{videoId}/playlist.m3u8
return videoId;
}
Embed Player
// Bunny Stream embeds via iframe
function BunnyPlayer({ videoId, libraryId }: { videoId: string; libraryId: string }) {
return (
<div style={{ position: "relative", paddingTop: "56.25%" }}>
<iframe
src={`https://iframe.mediadelivery.net/embed/${libraryId}/${videoId}?autoplay=false&loop=false&muted=false&preload=true`}
loading="lazy"
allow="accelerometer; gyroscope; autoplay; encrypted-media; picture-in-picture;"
allowFullScreen
style={{ border: "none", position: "absolute", top: 0, left: 0, width: "100%", height: "100%" }}
/>
</div>
);
}
Feature Comparison
| Feature | Mux | Cloudflare Stream | Bunny Stream |
|---|---|---|---|
| Adaptive bitrate | ✅ HLS | ✅ HLS | ✅ HLS |
| Direct browser upload | ✅ | ✅ (TUS) | ✅ |
| Custom player | ✅ Mux Player | ✅ Iframe/SDK | ✅ Iframe/SDK |
| Analytics | ✅ Mux Data | ✅ Basic | ✅ Basic |
| DRM | ✅ | ✅ | ✅ Widevine/FairPlay |
| Live streaming | ✅ RTMP | ✅ | ✅ |
| Signed URLs | ✅ | ✅ | ✅ |
| Captions/subtitles | ✅ Auto-generate | ✅ | ✅ |
| Webhook events | ✅ | ✅ | ✅ |
| Price/min stored | $0.015 | $0.005 | $0.0011 |
| Price/GB delivered | $0.045 | $0.001* | $0.0055 |
| Free tier | 100 min/month | 1,000 min/month | ❌ |
*Cloudflare Stream pricing uses minutes viewed model ($1/1k minutes); GB rate is approximate.
When to Use Each
Choose Mux if:
- Building a SaaS product with video as a core feature (education platform, content creation, video hosting)
- Per-viewer analytics (quality score, rebuffering rate, startup time) are important
- Developer experience is a priority — best SDK, best documentation, best DX
- Automatic captions and thumbnail generation are needed
- GIF preview generation for hover-to-preview UX
Choose Cloudflare Stream if:
- You're already a Cloudflare customer (domain, CDN, Workers)
- Simplest possible integration — minimal API surface
- Free 1,000 minutes/month is sufficient for testing or low-volume use
- Cost is the priority for low-volume use cases (cheapest per-minute storage)
Choose Bunny Stream if:
- High-volume video delivery where per-GB CDN cost matters most
- European data residency required (Bunny.net is EU-based)
- DRM is needed without enterprise pricing
- Budget-conscious — Bunny is consistently the cheapest option at scale
Methodology
Data sourced from official Mux documentation (docs.mux.com), Cloudflare Stream documentation (developers.cloudflare.com/stream), Bunny Stream documentation (docs.bunny.net/reference/api-overview), pricing pages as of February 2026, and community discussions from the Mux Discord and r/webdev.
Related: Fluent FFmpeg vs ffmpeg-wasm vs node-video-lib for self-hosted video processing before uploading, or Remotion vs Motion Canvas vs Revideo for programmatically generating video content to host on these platforms.