Skip to main content

Mux vs Cloudflare Stream vs Bunny Stream

·PkgPulse Team
0

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

FeatureMuxCloudflare StreamBunny 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 tier100 min/month1,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.

The 2026 JavaScript Stack Cheatsheet

One PDF: the best package for every category (ORMs, bundlers, auth, testing, state management). Used by 500+ devs. Free, updated monthly.