Skip to main content

S3 Object Storage: Provider Comparison and PHP Integration

Cloud & DevOps · 19.04.2026 · 4 min read
Illustration for “S3 Object Storage: Provider Comparison and PHP Integration”

Cloud object storage: comparing S3 providers

Object storage is the optimal way to store static assets, media files, backups, and ML datasets. All providers use an S3-compatible API, which makes switching providers easier.

Comparing S3 providers

ProviderPrice (GB/mo)Outbound trafficRegion
Hetzner Object Storage€0.0049€0.010/GB (first 1TB free)Germany, Finland
Backblaze B2$0.006$0.01/GBUS, EU
Cloudflare R2$0.015FREEGlobal
DigitalOcean Spaces$0.020$0.01/GB (first 1TB free)US, EU, Asia
AWS S3$0.023$0.09/GBGlobal

Cloudflare R2: free outbound traffic

# R2 via rclone
[cloudflare-r2]
type = s3
provider = Cloudflare
access_key_id = YOUR_R2_ACCESS_KEY
secret_access_key = YOUR_R2_SECRET_KEY
endpoint = https://ACCOUNT_ID.r2.cloudflarestorage.com
region = auto

Connecting to WordPress (WP Offload Media)

# wp-config.php
define('AS3CF_SETTINGS', serialize([
    'provider'    => 's3',
    'access-key-id'     => 'YOUR_KEY',
    'secret-access-key' => 'YOUR_SECRET',
    'bucket'      => 'my-wp-media',
    'region'      => 'eu-central-1',
    'domain'      => 'path',  // or a custom domain via CDN
]));

PHP: uploading files to S3

use Aws\S3\S3Client;

$s3 = new S3Client([
    'version'     => 'latest',
    'region'      => 'eu-central-1',
    'endpoint'    => 'https://YOUR_ENDPOINT',
    'credentials' => ['key' => 'KEY', 'secret' => 'SECRET'],
    'use_path_style_endpoint' => true,
]);

// Upload file
$s3->putObject([
    'Bucket'      => 'my-bucket',
    'Key'         => 'uploads/' . basename($_FILES['file']['name']),
    'SourceFile'  => $_FILES['file']['tmp_name'],
    'ACL'         => 'public-read',
    'ContentType' => $_FILES['file']['type'],
]);

// Get the public URL
$url = $s3->getObjectUrl('my-bucket', 'uploads/photo.jpg');
Recommendation: For European customers, use Hetzner Object Storage (the cheapest option for storage) or Cloudflare R2 (if outbound traffic matters). Both are compatible with the AWS S3 SDK.

Real S3 API Errors Explained

Two error messages show up most often for newcomers to object storage, and neither is caused by the network or a quota.

SignatureDoesNotMatch

<Error><Code>SignatureDoesNotMatch</Code><Message>The request signature we calculated does not match the signature you provided. Check your key and signing method.</Message></Error>

The classic causes: a wrong secret key (an extra space from copy-pasting), the server's system clock drifting by more than 15 minutes, or not setting use_path_style_endpoint => true for providers that do not support virtual-hosted style (MinIO, Hetzner). Check the time with timedatectl and sync it with chronyd or ntpd.

AccessControlListNotSupported

<Error><Code>AccessControlListNotSupported</Code><Message>The bucket does not allow ACLs</Message></Error>

Since April 2023, new AWS S3 buckets are created with Object Ownership set to Bucket owner enforced, which disables ACLs entirely. Code with 'ACL' => 'public-read' in putObject starts failing with this error. The fix is to drop the ACL parameter and grant access through a JSON bucket policy instead of an object-level ACL.

When Object Storage Is Not a Good Fit

  • You need POSIX semantics with file locking and atomic append — S3 is not a filesystem, it is a store for whole, immutable objects.
  • The application needs low latency for block-level database access — use local NVMe or network block storage, not an object API.
  • You are storing a huge number of very small files with a high request rate — billing for operations (PUT/GET) starts to exceed billing for volume, so it makes more sense to bundle files or put a CDN cache in front of the bucket.
  • You need real-time writes with minimal latency (streaming video, telemetry) — object storage was designed for batch writes, not a stream.
  • Regulatory requirements mandate physical control over the media — then deploy your own MinIO on your own hardware instead of a cloud provider.

Verifying the Result

# Upload a test file and check the checksum
aws s3 cp test.txt s3://my-bucket/test.txt --endpoint-url https://YOUR_ENDPOINT
aws s3api head-object --bucket my-bucket --key test.txt --endpoint-url https://YOUR_ENDPOINT

# Check public access if the bucket is supposed to be open
curl -I https://my-bucket.YOUR_ENDPOINT/test.txt

# Compare the local and remote MD5
md5sum test.txt
aws s3api head-object --bucket my-bucket --key test.txt --query ETag --endpoint-url https://YOUR_ENDPOINT

A 403 response code on a public curl -I almost always means the bucket policy does not allow anonymous reads, not a problem with access keys — keys are not needed at all to read a public object. If the object storage is used for backups, it makes more sense to run them through rclone than to write one-off PHP code for the task.

Was this article helpful?
← Back to Knowledge Base Ask Support