Offloading media to S3: a WordPress guide for developers
As WordPress sites grow, media storage becomes a real bottleneck. When you're serving high-res images and video to thousands of concurrent users, your web server's disk I/O tanks and page load times suffer. Offloading media to S3 is one of the most effective ways we've found to keep WordPress responsive while scaling uploads effortlessly.
Why S3 offloading matters in production
On a typical WordPress site, the /wp-content/uploads directory grows exponentially. By month six, you might have 50 GB of media. By year two, 500 GB. Your hosting provider's storage costs climb, backups take forever, and every media request competes with PHP processes for server resources.
S3 solves this elegantly. You get unlimited, predictable storage at around $0.023 per GB per month. Your server never touches the media files directly—S3 handles it. And when you pair S3 with CloudFront, your images are cached at edge locations worldwide, cutting latency from milliseconds to microseconds for users halfway around the globe.
We've seen median page load times drop by 40–60% after moving media off the primary WordPress server. That translates to better SEO rankings, lower bounce rates, and happier customers.
Plugins that sync uploads to S3
You don't need to build S3 integration from scratch. Mature WordPress plugins handle the heavy lifting: credential management, automatic uploads, URL rewriting, and even deletion cleanup.
Offload media Library to S3 (Delicious Brains) is the industry standard. It's battle-tested on six-figure monthly-traffic sites and handles edge cases gracefully. When you upload media through the WordPress admin or via REST API, it automatically pushes files to your S3 bucket. You can configure it to remove local copies after upload, freeing disk space immediately.
Here's a typical setup flow:
- Generate AWS IAM credentials with S3 permissions only (never use root credentials).
- Install and activate the plugin.
- Enter your bucket name, region, and credentials in plugin settings.
- Run a one-time sync of existing media.
- Verify URLs in the database are rewritten correctly.
WP Offload Media (the premium version of Delicious Brains' plugin) adds intelligent caching, delivery optimization, and white-label CDN support. For teams handling thousands of daily uploads, the premium features pay for themselves through reduced bandwidth costs and faster delivery.
StaticPress is a lighter-weight alternative if you want more control over the sync process. It's less automated but gives developers granular hooks to customize behavior.
For custom builds, the AWS SDK for PHP is always an option. Most teams, though, choose a plugin to avoid reinventing credential rotation, error handling, and URL rewriting.
Serving via CloudFront for global edge performance
S3 alone is fast, but CloudFront makes it lightning-quick. CloudFront is AWS's content delivery network (CDN). It caches your media at 600+ edge locations worldwide. When a user in Tokyo requests an image, they pull it from the Tokyo edge, not from your S3 bucket in us-east-1.
Setting up CloudFront is straightforward:
- Create a CloudFront distribution in the AWS Console.
- Set the origin to your S3 bucket.
- Enable Origin Access Identity (OAI) so only CloudFront can read from S3. Restrict the bucket policy to deny direct S3 access.
- Configure cache behaviors: typically 1 year for immutable assets like versioned images, 1 day for generic uploads.
- Assign a CNAME alias (e.g.,
media.example.com) to your distribution. - Add an SSL certificate (AWS Certificate Manager provides free ones).
Most S3 offload plugins integrate CloudFront seamlessly. You provide your CloudFront domain in plugin settings, and all media URLs automatically point to the CDN instead of S3 directly.
Here's a real scenario: an e-commerce site selling high-res product images. Without CloudFront, a user in Australia downloading a 5 MB image waits 2–3 seconds. With CloudFront, that same download completes in 300–500 ms because the image is cached at the Sydney edge. The difference isn't subtle—it affects conversion rates measurably.
Automatic URL rewriting: the silent hero
URL rewriting is where S3 offload plugins earn their keep. When you upload a file, WordPress stores metadata in the database. The plugin intercepts that workflow and rewrites the URL before it's saved.
A typical rewrite looks like this:
// Before
https://mysite.com/wp-content/uploads/2024/01/product.jpg
// After
https://media.mysite.com/wp-content/uploads/2024/01/product.jpg
The rewrite happens transparently. Post content, thumbnails, REST API responses—all point to the CDN. If you ever need to change CDN providers or switch back to local storage, the plugin can bulk-rewrite URLs with a single function call.
The plugin uses WordPress filters like wp_calculate_image_srcset and wp_get_attachment_url to intercept URL generation. This means even dynamic image resizing and srcset generation work correctly without manual intervention.
One thing to watch: if your theme or plugins generate media URLs with hardcoded domain names, those won't be rewritten. Always use WordPress functions like wp_get_attachment_url() and wp_get_attachment_image() instead of string concatenation.
Keeping backups of the bucket: your safety net
S3 gives you 11 nines of durability (99.999999999%), but that's different from backup. If a malicious actor or buggy code deletes 10,000 images, S3's durability won't help—those files are gone.
Always implement a backup strategy:
Versioning: Enable S3 versioning on your bucket. Every overwrite or delete preserves the previous version. You can restore deleted objects up to your retention window (typically 30–90 days). Versioning costs extra because you're storing multiple versions, but it's cheap insurance.
Cross-region replication: Configure S3 replication to sync your bucket to another region (e.g., us-east-1 to eu-west-1). If an entire AWS region goes down, your media is still available. This is rare but has happened.
Glacier archive: Set up a lifecycle policy to move old media to Glacier (cheaper long-term storage) after 90 days. If you ever need an image from six months ago, you can restore it from Glacier—it takes 1–5 hours and costs less than S3 storage.
Here's a sample lifecycle policy you'd set in AWS:
| Rule name | Status | Action | Days |
| archive-old-media | Enabled | Transition to Glacier | 90 |
| delete-very-old | Enabled | Expire | 730 (2 years) |
Combine this with nightly database backups (which store attachment metadata), and you can recover any media scenario short of a catastrophic AWS failure.
Common mistakes we see teams make
Using root AWS credentials: Never paste your root AWS access key into WordPress. Create an IAM user with S3-only permissions. Rotate credentials every 90 days.
Forgetting OAI: If you set up CloudFront but leave your S3 bucket public, users can bypass the CDN and hit S3 directly, wasting money on bandwidth. Always use Origin Access Identity and a restrictive bucket policy.
Not testing bulk operations: When you first enable S3 offloading on a site with 100,000 existing images, don't just hit "sync all." Test with 1,000 images first. Monitor S3 API throttling, database locks, and memory usage. A bad bulk sync can take down a live site.
Skipping URL validation: After enabling S3 offloading, spot-check 10–20 random media URLs. Visit them directly. Verify thumbnails, responsive images, and srcset generation. We've seen cases where only srcset URLs were rewritten, breaking responsive images.
Ignoring CloudFront cache invalidation: If you delete a file from S3, CloudFront might still serve the cached version for hours. Either set aggressive cache TTLs (1 hour) for frequently updated media, or implement cache invalidation in your plugin workflow.
Key takeaways
- S3 offloading shifts media storage and I/O load off your primary WordPress server, improving performance and scalability.
- Plugins like Offload Media handle credential management, automatic uploads, and URL rewriting—use them instead of building custom integrations.
- Pair S3 with CloudFront to cache media at global edge locations, cutting latency by 80–90% for geographically distributed users.
- URL rewriting is automatic and transparent; always use WordPress media functions to generate URLs so the plugin can intercept and rewrite them.
- Implement S3 versioning, cross-region replication, and lifecycle policies to protect against accidental deletion or regional failures.
- Test bulk sync operations on a staging environment first; spot-check URLs and responsive images before enabling on production.
- Use IAM credentials with S3-only permissions, and rotate them regularly. Never hardcode credentials in code.






Comments
Loading comments…