SCALING
August 5, 2026

How to Use Object Storage to Offload Your Server

6 min read
Author
CloudStick Team
WordPress Engineer
Share this article
How to Use Object Storage to Offload Your Server
CloudStick
Offload Your
Media Files

What Object Storage Actually Is

Object storage is a flat, API-driven store for files that lives outside your server's own disk — you upload an object over HTTPS, get back a URL, and never think about which physical drive it sits on again. Instead of a filesystem with directories and inodes, you get a bucket, a key, and a value, accessed through the S3 API that AWS S3, DigitalOcean Spaces, Backblaze B2, Cloudflare R2, and Wasabi all speak more or less identically.

For WordPress and WooCommerce specifically, that means the media library, product images, user avatars, and any video or PDF uploads stop being files on your server's local disk and become objects in a bucket that your PHP application reads and writes to over the network. The site's PHP code, templates, and database stay exactly where they are — only the uploaded assets move.

Why Local Disk Becomes a Bottleneck

Local disk is finite and it fills up quietly. A typical CloudStick server ships with 150GB of disk shared across the OS, MariaDB's data files, logs, backups, and every site's wp-content/uploads directory — and on an active WooCommerce store, product image uploads alone can eat several gigabytes a month without anyone noticing until the disk-usage graph is pinned near 100%.

Once local disk is full, the failure mode is ugly and not obviously about uploads at all: MySQL can fail to write, PHP-FPM can fail to write session files, and backups can fail silently mid-run. Offloading media to object storage is one of the highest-leverage ways to extend a server's effective life, because it removes the single fastest-growing, least-compressible category of data from the disk that everything else depends on.

A server's local disk is a single point of failure hiding in plain sight — it's the one resource every site, every backup job, and every database write silently depends on, and the moment you scale beyond one box, whatever lives only on that disk becomes invisible to everything else you add.

Why It Becomes Mandatory Once You Go Multi-Server

On a single server, local uploads work fine — the file is written and served from the same box on every request. The moment you put a load balancer in front of two or more app servers, that stops being true: a customer's order-confirmation upload or a new blog post's featured image lands on whichever server happened to handle that request, and every other server in the pool has no idea the file exists.

The symptom is maddening because it's intermittent: broken images that only appear for some visitors depending on which backend server the load balancer routed them to, product photos that vanish and reappear on refresh, admin uploads that "disappear" until someone happens to hit the right server again. Object storage removes the ambiguity entirely — every server reads and writes to the same bucket, so there is no "which disk is the truth" question once you're running more than one app server.

Choosing an S3-Compatible Provider

Pick a provider that speaks the S3 API rather than a proprietary storage API, so you're never locked into one vendor's SDK or CLI. AWS S3 is the reference implementation and the most feature-complete, but it charges for egress bandwidth on every byte served to a visitor. DigitalOcean Spaces and Wasabi bundle a generous amount of free egress into flat monthly pricing, which suits media-heavy WordPress sites well. Backblaze B2 is usually the cheapest per-gigabyte for storage itself. Cloudflare R2 charges nothing for egress at all, which matters a lot once a site serves meaningful image or video traffic.

Because every one of these speaks the same S3 API, the aws-cli and every WordPress offload plugin work against all of them with nothing more than a different endpoint URL and access key pair — you can start on one provider and migrate to another later using the exact same sync command.

Setting Bucket Permissions Correctly

Use two buckets with two different permission models, not one bucket for everything. Media that WordPress serves to visitors — the media library, product images, avatars — needs public-read so browsers can load the URLs directly. Backups and any private uploads need to stay fully private, readable only with signed credentials, because a publicly listable backup bucket is a direct path to a full site compromise.

{
"Version": "2012-10-17",
"Statement": [{
"Sid": "PublicReadMedia",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::mysite-media/*"
}]
}

Apply that policy only to the media bucket. Leave the backups bucket with block-all-public-access enabled and no bucket policy granting anonymous reads at all, so the only way in is with your server's own access keys.

Routing New Uploads to the Bucket

For WordPress, the fastest path is an offload plugin in the WP Offload Media family: install it, enter the bucket name, region, and access key/secret for your chosen provider, and it intercepts every new media upload, writes it straight to the bucket, and rewrites the URL WordPress stores in the database to point at the bucket (or a CDN in front of it) instead of a local path.

For a custom PHP application or a headless setup, integrate the provider's SDK directly — the AWS SDK for PHP, or any S3-compatible client library — and call putObject on your upload handler instead of writing to a local path with file_put_contents or move_uploaded_file. Either approach achieves the same outcome: new uploads never touch local disk at all, which is what actually solves the multi-server visibility problem from earlier.

Migrating Existing Files and Adding a CDN

Routing new uploads to the bucket doesn't move the years of existing files already sitting in wp-content/uploads — you have to sync them separately. The aws-cli's sync command is the standard tool for this, and it works identically against S3, Spaces, B2 (via its S3-compatible endpoint), or R2, since they all implement the same API.

aws s3 sync /home/<username>/apps/<sitename>/wp-content/uploads \
s3://mysite-media/wp-content/uploads \
--acl public-read \
--endpoint-url https://<region>.digitaloceanspaces.com
# s3cmd alternative:
s3cmd sync --acl-public /home/<username>/apps/<sitename>/wp-content/uploads/ \
s3://mysite-media/wp-content/uploads/

Run sync once for the bulk migration, then run it again (it's incremental and only copies changed or new files) right before you flip the offload plugin's "serve from bucket" setting on, so nothing uploaded in between gets missed. After the switch, put a CDN in front of the bucket rather than serving directly — this is where CloudStick's native Cloudflare integration pays off, since the same DNS and edge-caching setup you'd already use for the site's domain can front the media bucket too, giving cached, edge-delivered images without standing up a separate CDN account.

In practice: choose an S3-compatible provider, create a public-read media bucket and a private backups bucket, install an offload plugin or wire up the SDK so new uploads go straight to the bucket, run aws s3 sync once to migrate everything already on disk, then point Cloudflare at the bucket for CDN delivery. From that point on, local disk on the CloudStick server holds only code, database, and logs — the part of the stack that actually needs to stay local — and you can add a second app server behind a load balancer without every other image on the site quietly breaking.

Leave a comment
Full Name
Email Address
Message
Contents