September 23, 2026 IZHubs

How to Offload WordPress Media to Cloudflare R2 (Complete Zero-SDK Setup Guide)

Step-by-step technical guide to offloading WordPress media to Cloudflare R2 using iz-r2-media with zero AWS SDK footprint, custom CDN domains, and 12-factor configuration.

Running WordPress media libraries directly on VPS local disks creates persistent operational bottlenecks: server disk space fills up rapidly, automated backup archives balloon to gigabytes, and single-server image delivery degrades latency for international visitors.

Cloudflare R2 provides an S3-compatible cloud object storage service featuring zero egress bandwidth fees and standard storage pricing of $0.015 per GB-month. However, legacy WordPress offload plugins typically bundle the official Amazon AWS PHP SDK—introducing over 35MB of third-party vendor code and consuming 15MB to 30MB of PHP runtime RAM per request.

This guide walks through configuring iz-r2-media, a lightweight, zero-SDK media offloader built with a native pure-PHP AWS Signature Version 4 (SigV4) cURL transport (under 60KB total code, under 1MB RAM overhead).


Technical Architecture Overview

The iz-r2-media engine replaces the standard WordPress media upload and retrieval pipeline with non-blocking, cloud-native storage primitives:

┌────────────────────────────────────────────────────────────────────────┐
│                      iz-r2-media Storage Pipeline                      │
│                                                                        │
│   1. Media Upload (WP Admin / REST API)                                │
│      │                                                                 │
│      ├── wp_generate_attachment_metadata                               │
│      └── Intercepted by UploadHandler                                  │
│          │                                                             │
│          ├── Pure PHP AWS SigV4 Signer (HMAC-SHA256, Zero-SDK)         │
│          ├── Direct HTTP PUT stream via cURL to Cloudflare R2          │
│          └── Attachment PostMeta marked: _iz_r2_offloaded = 1          │
│                                                                        │
│   2. Frontend Delivery & URL Rewriting                                 │
│      │                                                                 │
│      ├── wp_get_attachment_url & wp_calculate_image_srcset             │
│      └── Rewritten to Custom CDN Domain (https://media.example.com/..) │
│                                                                        │
│   3. Administrative Safety (keep_local = true by default)              │
│      │                                                                 │
│      ├── Local copy preserved on VPS disk                              │
│      └── Zero 404 errors if plugin is temporarily deactivated          │
└────────────────────────────────────────────────────────────────────────┘

Step 1: Obtain Cloudflare R2 Credentials & Parameters

To configure iz-r2-media, you need 5 configuration values from your Cloudflare account:

  1. Account ID
  2. Bucket Name
  3. Access Key ID
  4. Secret Access Key
  5. Custom CDN Domain

Here is the exact procedure to extract each value from the Cloudflare Dashboard.

1. Find Your Cloudflare Account ID

There are two fast methods to locate your Account ID:

  • From the URL: Look at your browser address bar when logged into Cloudflare. The 32-character hexadecimal string immediately following dash.cloudflare.com/ is your Account ID:
    https://dash.cloudflare.com/c3f4e289a0b123456789abcdef012345/r2/overview
                                └───────────────────────────────┘
                                        Your Account ID
  • From the R2 Overview Sidebar: Navigate to R2 in the left menu. In the right-hand sidebar under Account details, click the copy icon next to Account ID.

2. Create Your R2 Storage Bucket (Bucket Name)

  1. In Cloudflare Dashboard, click R2 in the left navigation menu.
  2. Click the blue Create bucket button.
  3. Enter your Bucket name (e.g., prod-wp-media).
    • Use lowercase alphanumeric characters and hyphens only.
    • This exact name is your Bucket Name parameter.
  4. Choose a Location hint (e.g., APAC for Asia-Pacific or WNAM for Western North America) closest to your primary audience.
  5. Click Create Bucket.

3. Generate Scoped API Tokens (Access Key ID & Secret Access Key)

To adhere to least-privilege security principles, generate an R2 API token restricted exclusively to your WordPress media bucket:

  1. From the R2 Overview page, click Manage R2 API Tokens in the top-right corner.
  2. Click Create API token.
  3. Configure the token permissions:
    • Token name: Enter an identifier such as WordPress iz-r2-media.
    • Permissions: Select Object Read & Write. (Do not select “Admin Read & Write” to avoid granting bucket deletion privileges).
    • Specify bucket(s): Choose Apply to specific buckets only, then select the bucket created above (prod-wp-media).
    • Client IP Address Filtering: Optional. Leave blank for standard web servers.
    • TTL / Expiration: Leave unset for permanent production use, or set per your compliance schedule.
  4. Click Create API Token at the bottom of the page.
  5. Save Your Credentials Immediately: Cloudflare will display a credentials summary screen:
    • Access Key ID: Copy the 32-character hexadecimal string.
    • Secret Access Key: Copy the 64-character hash string.
    • Important Security Note: The Secret Access Key is displayed only once. If you close or refresh this tab before copying, it cannot be recovered and you will need to generate a new token.

4. Connect a Custom CDN Domain (Custom Domain)

Connecting a custom subdomain (e.g., media.example.com) ensures all media files are served through Cloudflare edge caching under your own SSL-secured domain:

  1. In Cloudflare Dashboard, go to R2 and click into your bucket (prod-wp-media).
  2. Click the Settings tab.
  3. Scroll down to the Public access section.
  4. Under Custom Domains, click Connect Domain.
  5. Enter your desired subdomain: media.example.com. (The root domain example.com must be an active zone in your Cloudflare account).
  6. Click Continue, review the DNS CNAME record that Cloudflare will automatically create, and click Connect Domain.
  7. Wait 30 to 60 seconds until the status shows Active.
  8. Your Custom Domain value to paste into WordPress is: https://media.example.com (must include the https:// prefix).

Cloudflare-to-WordPress Parameter Mapping Matrix

Parameter in WordPress PluginSource in Cloudflare DashboardExample Value
Account IDR2 Overview -> Right Sidebar -> Account detailsc3f4e289a0b123456789abcdef012345
Bucket NameR2 -> Bucket Name created in Step 1.2prod-wp-media
Access Key IDR2 API Tokens -> Create API Token screen7a19cb02e123456789abcdef01234567
Secret Access KeyR2 API Tokens -> Generated token screen (shown once)99f01e44d123456789abcdef0123456789abcdef...
Custom DomainR2 Bucket -> Settings -> Public access -> Custom Domainshttps://media.example.com
Keep Local FilesWordPress Settings checkbox (keep_local)Checked (Preserves local disk copies)

Step 2: Install and Activate iz-r2-media

Download and install the plugin using your preferred administration method.

Method A: WordPress Admin Dashboard

  1. Download iz-r2-media.zip from the official repository or release page.
  2. In WordPress Admin, navigate to Plugins -> Add New -> Upload Plugin.
  3. Select iz-r2-media.zip and click Install Now.
  4. Click Activate Plugin.

Method B: WP-CLI

wp plugin install https://github.com/izhubs/iz-r2-media/releases/latest/download/iz-r2-media.zip --activate

Step 4: Configure Storage Parameters

The plugin supports two configuration layers: Web Admin Settings (wp_options) and Twelve-Factor Environment Constants.

Option A: Web Admin Settings UI

Navigate to Settings -> Cloudflare R2 Media in your WordPress dashboard:

Field LabelParameter KeyDescriptionExample Value
Account IDaccount_idCloudflare Account IDc3f4e2...89a
Access Key IDaccess_keyScoped R2 Access Key ID7a19c...b02
Secret Access Keysecret_keyScoped R2 Secret Access Key99f01e...44d
Bucket NamebucketTarget R2 bucket nameprod-wp-media
Custom Domaincustom_domainConnected Cloudflare CDN domainhttps://media.example.com
Keep Local Fileskeep_localRetain local files on VPS diskChecked (Default)

Click Save Changes. The plugin executes a live SigV4 test ping against your R2 bucket and displays connection status.

For containerized setups (Docker, Roots Bedrock, izdeploy), define constants in wp-config.php or inject them through system environment variables. This prevents API keys from being stored in database dumps and locks the web UI against unauthorized modifications:

// wp-config.php
define( 'IZ_R2_ACCOUNT_ID', 'c3f4e289a0b123456789abcdef012345' );
define( 'IZ_R2_ACCESS_KEY', '7a19cb02e123456789abcdef01234567' );
define( 'IZ_R2_SECRET_KEY', '99f01e44d123456789abcdef0123456789abcdef0123456789abcdef01234567' );
define( 'IZ_R2_BUCKET', 'prod-wp-media' );
define( 'IZ_R2_CUSTOM_DOMAIN', 'https://media.example.com' );
define( 'IZ_R2_KEEP_LOCAL', true );

When constants are defined, all settings fields in the WordPress Admin UI display as read-only with an administrative badge: Locked: Controlled via wp-config.php.


Step 5: Migrate Existing Media Library

Existing media files uploaded prior to plugin installation require migration to Cloudflare R2.

Web Migration (Action Scheduler)

  1. In WordPress Admin, navigate to Settings -> Cloudflare R2 Media.
  2. Scroll to the Bulk Media Migration card.
  3. Click Start Background Migration.
  4. The migration engine queues tasks in batches of 25 items using Action Scheduler, preventing PHP execution timeouts. A progress bar updates in real time.

High-Concurrency WP-CLI Migration (Pro Edition)

For catalogs exceeding 10,000 files, the commercial Pro add-on (iz-r2-media-pro) provides socket-level multiplexed migration via cURL multi-handle pooling:

# Execute 8-stream parallel migration
wp iz-r2 sync --concurrency=8

# Migrate with 16 parallel sockets and unlink verified local files
wp iz-r2 sync --concurrency=16 --delete-local --batch-size=100

# Perform dry-run simulation
wp iz-r2 sync --dry-run

Step 6: Deactivation Lifecycle & Safe Rollback

Understanding plugin deactivation mechanics is essential for operational reliability:

1. New Uploads After Deactivation

When iz-r2-media is deactivated, WordPress unhooks the upload handlers. Newly uploaded media files immediately revert to standard core storage (wp-content/uploads/ on local VPS disk).

2. Previously Offloaded Media

  • With Default Setting (keep_local = true): Local file copies remain on your VPS disk. When the plugin is deactivated, WordPress URL rewriting filters detach. Media URLs automatically revert to your primary domain (https://example.com/wp-content/uploads/...). Assets continue serving directly from your web server without broken images or 404 errors.
  • With Disk Savings Setting (keep_local = false): Local files were removed upon upload to save server disk. Deactivating the plugin causes WordPress core to query local paths that no longer exist, producing HTTP 404 responses. Prior to deactivating under this configuration, download your media assets from Cloudflare R2 back to wp-content/uploads/.

Summary Specification

Technical MetricBenchmark
Plugin Package Footprint54 KB (Community) / 30 KB (Pro Add-on)
PHP Runtime Memory OverheadUnder 1.0 MB
External Dependencies0 (Native cURL + PHP OpenSSL extensions)
WordPress CompatibilityWordPress 6.0 to 6.7+
PHP Runtime CompatibilityPHP 8.1, 8.2, 8.3, 8.4
Supported Storage EndpointsCloudflare R2 (S3 API Standard)
LicenseGPLv2 or later