Object storage looks like "drop it and forget it" — but without operational discipline, buckets turn into dozens of terabytes of orphaned data with an unpredictable bill. Let's look at how to set up backups correctly, protect yourself against data loss, and avoid overpaying.
S3 as a place for backups
S3 is a convenient target for backups of any system: PostgreSQL dumps, MongoDB and Elasticsearch snapshots, configuration files, encrypted secrets.
Why S3 fits this role well:
- 99.999999999% durability (11 nines) — data is stored across multiple availability zones within a single region.
- Cold storage classes — Glacier Deep Archive costs about $1 per terabyte per month; storing year-old daily backups is nearly free.
- Versioning protects against accidental deletion and ransomware: a "deleted" object actually gets a delete marker, and the old version stays.
- Replication to another region provides disaster recovery.
A typical backup bucket structure looks like this:
s3://backup-bucket/
├── pg/ ← PostgreSQL dumps
├── mongo/ ← MongoDB dumps
├── es/ ← Elasticsearch snapshots
└── app/ ← application exports
On the bucket you enable: versioning, a lifecycle policy (transition to Glacier after 30 days, deletion of old versions after 90), and replication to another region.
Cross-region replication
AWS gives you 11 nines of durability, but the main enemy of data is human error: accidentally deleting a bucket, a compromised account.
Cross-Region Replication (CRR) solves both problems. Every object placed into the source bucket is asynchronously copied to a destination bucket in another region (or account). If something happens to the production account, the backup is out of the attacker's reach.
Configuration via AWS CLI:
{
"Role": "arn:aws:iam::ACCOUNT:role/replication-role",
"Rules": [{
"Status": "Enabled",
"Destination": {
"Bucket": "arn:aws:s3:::backup-dr-bucket",
"StorageClass": "STANDARD_IA"
},
"DeleteMarkerReplication": { "Status": "Enabled" }
}]
}
An important point: replication works only for new objects. Data that already sits in the bucket is not replicated automatically — for that you need aws s3 sync or S3 Batch Replication.
Versioning and MFA Delete
Versioning is enabled at the bucket level. Once enabled, every PUT creates a new object version, and a DELETE adds a delete marker — physically the object stays.
For critical backup buckets, add MFA Delete: deleting objects requires a one-time code from the root account's MFA device. Even if a developer's account is compromised, the backups can't be destroyed.
aws s3api put-bucket-versioning \
--bucket backup-bucket \
--versioning-configuration Status=Enabled,MFADelete=Enabled \
--mfa "arn:aws:iam::ACCOUNT:mfa/user 123456"
Object Lock is even stricter: WORM mode (write-once-read-many) makes deletion physically impossible for a set period, even for an administrator. It's used in compliance buckets for financial and medical data.
Lifecycle policies for different cases
Lifecycle manages transitions between storage classes and object deletion. A few typical sets:
User content (avatars, uploads):
- prefix: "tmp/"
Expiration: 7 days
- prefix: "users/"
NoncurrentVersionExpiration: 30 days
AbortIncompleteMultipartUpload: 7 days
Logs and audit:
- prefix: "audit/"
Transitions:
- 30 days → STANDARD_IA
- 90 days → GLACIER_FLEXIBLE
- 365 days → DEEP_ARCHIVE
Expiration: 2555 days # 7 years for compliance
PostgreSQL backups:
- prefix: "pg/"
Transitions:
- 7 days → STANDARD_IA # fast recovery for the first week
- 30 days → GLACIER_INSTANT # access within minutes
- 90 days → DEEP_ARCHIVE # long-term storage
Expiration: 365 days
NoncurrentVersionExpiration: 30 days
AbortIncompleteMultipartUpload: 1 day
The AbortIncompleteMultipartUpload rule is worth adding everywhere: incomplete multipart uploads accumulate unnoticed and get billed too.
What makes up the cost of S3
AWS S3 bills you across three line items.
Storage
You pay per gigabyte per month. The storage class determines the price:
| Class | $/GB/month (us-east-1) | Use |
|---|---|---|
| Standard | $0.023 | Active data |
| Standard-IA | $0.0125 | Backups, accessed once a month |
| Glacier Instant | $0.004 | Archive with fast-access capability |
| Glacier Flexible | $0.0036 | Compliance, access within hours |
| Deep Archive | $0.00099 | Cold archive, access within a day |
100 TB on Standard is about $2300 per month. On Deep Archive, about $100 per month.
Requests
PUT/COPY/POST/LIST: ~$0.005 per 1000 requests. GET: ~$0.0004 per 1000 requests.
For most applications these are negligible amounts. But if the application writes each event as a separate file, the bill grows. A million LIST operations per day = $5/day = $150 per month.
A typical mistake: writing a million 1 KB files instead of one 1 GB file. The storage cost is the same, but the request cost is a million times higher.
Egress traffic
The most insidious line item. Traffic from S3 to the internet: ~$0.09 per GB.
100 TB per month = $9000. For public buckets with media content, this line item often exceeds the cost of the storage itself.
Ways to reduce it:
- CloudFront in front of S3 — caching at edge servers reduces egress traffic from S3; CloudFront → user traffic is cheaper ($0.085/GB at volumes above 10 TB).
- VPC Endpoint for traffic from EC2 in the same region — free. For backend services running in AWS, this should always be enabled.
Alternatives to AWS S3
| Provider | Egress traffic | Notable feature |
|---|---|---|
| AWS S3 | $0.09/GB | The standard |
| Backblaze B2 | $0.01/GB | S3-compatible API, cheap traffic |
| Cloudflare R2 | free | S3-compatible API, no traffic charges |
| Wasabi | free (with limits) | Cheap storage |
| Yandex Object Storage | ~$0.005–0.01/GB | Russian jurisdiction |
Cloudflare R2 is especially interesting for public content with high traffic: its S3-compatible API lets you switch via endpointOverride in the SDK without rewriting code.
Monitoring
CloudWatch metrics
S3 automatically sends metrics to CloudWatch:
| Metric | What it means | Reason to alert |
|---|---|---|
BucketSizeBytes | Bucket size (once a day) | A sudden spike |
NumberOfObjects | Object count (once a day) | Growth of thousands per day |
AllRequests / 4xxErrors / 5xxErrors | Requests and errors | Errors > 0.1% |
BytesDownloaded | Egress traffic | Basis for estimating cost |
Access logs
Server access logging writes each request to a text file in another bucket: IP, time, action, object key, status, size, latency, user-agent.
aws s3api put-bucket-logging --bucket production-bucket \
--bucket-logging-status '{
"LoggingEnabled": {
"TargetBucket": "audit-bucket",
"TargetPrefix": "logs/production-bucket/"
}
}'
An alternative is CloudTrail data events: JSON format, slightly higher latency, but it integrates with the rest of your CloudTrail events.
S3 Inventory
S3 Inventory generates a daily (or weekly) report on all objects in a bucket in CSV or Parquet format. Columns: key, size, storage class, last-modified date, encryption status.
It's useful for verifying that a lifecycle policy works as expected, and for finding "orphans" — objects that have no records in the database.
Common problems and protection
Account compromise and data deletion
Scenario: an attacker obtained an account with s3:DeleteObject permissions and deleted everything.
Protection:
- Versioning — objects aren't physically deleted, only delete markers are added. Recovery via
aws s3api list-object-versions. - MFA Delete — bulk deletion requires the root account's MFA token.
- Object Lock — physical deletion is impossible for a set period.
- Replication to another account — the backup is out of reach for the compromised account.
Objects moved to Deep Archive by mistake
A lifecycle policy with an imprecise filter got applied to hot data. Reading from Deep Archive takes up to 12 hours and costs money.
Protection: always test a lifecycle policy on a small test prefix before applying it to the whole bucket.
Recovery: a restore-object request, waiting (up to 12 hours), then copying to Standard.
A bucket accidentally became public
Erroneous edits to the bucket policy made the whole bucket accessible from outside. This risks data leakage and a huge egress bill.
Protection: Block Public Access at the account level — a global switch that prevents any bucket from becoming public, even if someone changes a policy. Always enable it on the root account.
Millions of small files — an unexpected bill
Every event is logged as a separate PUT. After a month — millions of files and noticeable request costs.
Solution: accumulate events locally and flush them to S3 in batches every few minutes or upon reaching a size threshold. Amazon Kinesis Data Firehose does this automatically.
Production bucket checklist
The minimum set of settings that every production bucket should have:
- Block Public Access enabled (at the account level too)
- Versioning enabled
- SSE-S3 or SSE-KMS (enabled by default for new buckets since 2023; for older ones, check)
- Lifecycle policy: aborting incomplete multipart uploads after 7 days, deleting old versions, transitioning to cold classes
- CloudWatch alert on a sharp increase in size and object count
- Access logs or CloudTrail data events for auditing
- Cross-region replication for critical buckets
- VPC Endpoint for traffic from EC2/EKS
- MFA Delete on backup buckets
- IAM with least privilege —
s3:*without restrictions is unacceptable
In short
- S3 is a durable place for database and file backups; Glacier Deep Archive lowers the cost of long-term storage to $1/TB per month.
- Versioning protects against accidental deletion and ransomware — an object is only physically deleted when its version expires.
- Cross-Region Replication copies new objects to another region or account; for objects that already exist you need
aws s3 sync. - MFA Delete blocks bulk deletion even when an account is compromised.
- S3 cost is made up of storage, requests, and egress traffic; traffic is the most insidious line item ($0.09/GB to the internet).
- VPC Endpoint makes traffic from EC2 to S3 within a single region free.
- Cloudflare R2 is an S3-compatible option with no egress charges; switching is done via
endpointOverride. - CloudWatch, access logs, and S3 Inventory cover the main monitoring needs.
What to read next
- Fundamentals — how S3 is built: bucket, object, key, storage classes.
- Spring + AWS SDK v2 for S3 — working with S3 from a Java application.
- Operating Elasticsearch — S3 as a target for Elasticsearch snapshots.