← Back to DevBytes

S3 Best Practices: Cost, Security, and Performance

Introduction to Amazon S3 Best Practices

Amazon Simple Storage Service (S3) is one of the most widely used object storage services in the world, powering everything from data lakes and backup archives to static websites and machine learning datasets. However, the flexibility that makes S3 so powerful also makes it easy to misuse. Without a deliberate strategy, teams often end up with bloated bills, exposed data, and slow applications.

This tutorial walks through the three pillars of running S3 at scale: cost optimization, security hardening, and performance tuning. Each section includes concrete configuration steps, code examples, and the reasoning behind each recommendation so you can apply these practices confidently in production.

What Is Amazon S3?

Amazon S3 is an object storage service that stores data as objects within buckets. Each object is identified by a unique key, and buckets act as top-level containers with their own permissions, policies, and configurations. S3 provides 99.999999999% (11 nines) of durability by redundantly storing objects across multiple facilities, making it suitable for virtually any workload that requires durable, scalable storage.

Because S3 is a managed service, AWS handles replication, hardware failures, and scaling. Your responsibility is to configure buckets correctly, manage access, and structure data in a way that performs well and stays cost-effective as it grows.

Why Best Practices Matter

S3 is deceptively simple to start using. A single API call can create a bucket and upload a file, which is great for prototyping but dangerous for production. Common pitfalls include:

Following best practices from the start prevents these issues and keeps your storage infrastructure predictable as data volumes grow into terabytes or petabytes.

Cost Optimization Best Practices

Choose the Right Storage Class

S3 offers multiple storage classes, each priced differently based on access frequency and retrieval cost. Matching data access patterns to the correct class is the single biggest cost lever available.

Use Lifecycle Rules to Automate Transitions

Lifecycle rules automatically transition objects between storage classes or delete them after a defined period. This removes the need for manual cleanup and ensures you never pay for Standard storage on data that has not been touched in months.

The following AWS CLI command applies a lifecycle configuration that moves logs to Standard-IA after 30 days, to Glacier after 90 days, and deletes them after 365 days:

{
  "Rules": [
    {
      "ID": "LogLifecycleRule",
      "Status": "Enabled",
      "Filter": {
        "Prefix": "logs/"
      },
      "Transitions": [
        {
          "Days": 30,
          "StorageClass": "STANDARD_IA"
        },
        {
          "Days": 90,
          "StorageClass": "GLACIER"
        }
      ],
      "Expiration": {
        "Days": 365
      }
    }
  ]
}

Apply it with:

aws s3api put-bucket-lifecycle-configuration \
  --bucket my-application-logs \
  --lifecycle-configuration file://lifecycle.json

Enable S3 Versioning Cleanup

Versioning protects against accidental deletes, but every version is billed as a separate object. Without lifecycle rules on noncurrent versions, costs can balloon quickly. Add a NoncurrentVersionExpiration rule to prune old versions:

{
  "Rules": [
    {
      "ID": "PruneOldVersions",
      "Status": "Enabled",
      "Filter": {},
      "NoncurrentVersionExpiration": {
        "NoncurrentDays": 90
      }
    }
  ]
}

Use S3 Batch Operations and Storage Lens

S3 Storage Lens provides organization-wide visibility into storage usage and trends, helping you spot buckets with anomalous growth or objects that should have been transitioned. S3 Batch Operations can then act at scale, for example copying millions of objects into a cheaper storage class or deleting stale prefixes.

Security Best Practices

Block Public Access by Default

The single most important security control is S3 Block Public Access. Enable it at the account level so that no bucket, even one created by mistake, can be made public. This setting overrides individual bucket policies and ACLs.

aws s3api put-public-access-block \
  --account-id 123456789012 \
  --public-access-block-configuration \
    BlockPublicAcls=true,\
    IgnorePublicAcls=true,\
    BlockPublicPolicy=true,\
    RestrictPublicBuckets=true

Enforce Encryption at Rest

Enable default encryption on every bucket so that all new objects are encrypted automatically. You can use server-side encryption with S3-managed keys (SSE-S3), KMS-managed keys (SSE-KMS), or customer-provided keys (SSE-C). SSE-KMS is recommended when you need audit trails and granular key policies.

aws s3api put-bucket-encryption \
  --bucket my-secure-bucket \
  --server-side-encryption-configuration '{
    "Rules": [
      {
        "ApplyServerSideEncryptionByDefault": {
          "SSEAlgorithm": "aws:kms",
          "KMSMasterKeyID": "arn:aws:kms:us-east-1:123456789012:key/abcd1234-..."
        }
      }
    ]
  }'

Use Least-Privilege Bucket Policies

Avoid granting s3:* or broad s3:GetObject on "Resource": "*". Scope policies to specific buckets and prefixes, and condition on tags or source VPC endpoints when possible. The following policy allows a specific role to read only objects under the reports/ prefix:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowReportsRead",
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::123456789012:role/AnalyticsRole"
      },
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::my-secure-bucket/reports/*"
    }
  ]
}

Enforce Encryption and HTTPS in Transit

Add a bucket policy condition that denies any request not using TLS. This prevents clients from sending data over plaintext HTTP:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyInsecureTransport",
      "Effect": "Deny",
      "Principal": "*",
      "Action": "s3:*",
      "Resource": [
        "arn:aws:s3:::my-secure-bucket",
        "arn:aws:s3:::my-secure-bucket/*"
      ],
      "Condition": {
        "Bool": {
          "aws:SecureTransport": "false"
        }
      }
    }
  ]
}

Enable Access Logging and CloudTrail

Turn on S3 server access logs for audit-sensitive buckets and ensure CloudTrail data events are capturing GetObject and PutObject calls. Combine these with AWS Config rules such as s3-bucket-public-read-prohibited to detect drift automatically.

Performance Best Practices

Use Prefix Spreading to Avoid Hotspots

S3 automatically scales by partitioning based on request rate. Historically, prefixes with high request rates could become bottlenecks. While S3 now supports very high request rates per prefix, it is still good practice to spread keys across multiple prefixes for workloads exceeding a few thousand requests per second.

Instead of naming keys sequentially like logs/2024/01/01/file1.log, prepend a hash or shard identifier:

logs/0a/2024/01/01/file1.log
logs/1f/2024/01/01/file2.log
logs/c7/2024/01/01/file3.log

This spreads writes across multiple index partitions and improves parallel throughput for high-volume ingestion pipelines.

Use Multipart Upload for Large Objects

For objects larger than 100 MB, use multipart upload. It improves reliability, allows parallel uploads of parts, and lets you retry only failed parts instead of the entire object. The AWS SDKs handle this automatically when you set a threshold, but you can also drive it directly:

aws s3 cp large-file.tar.gz s3://my-bucket/large-file.tar.gz \
  --expected-size 5000000000 \
  --part-size 64MB

Use Byte-Range Fetches for Parallel Reads

When reading large objects, request byte ranges in parallel to maximize throughput. This is especially useful for analytical workloads that only need a slice of a large file:

aws s3api get-object \
  --bucket my-bucket \
  --key large-file.bin \
  --range "bytes=0-1048575" \
  part-0.bin

Use S3 Transfer Acceleration for Cross-Region Uploads

For uploads from geographically distributed clients, enable S3 Transfer Acceleration. It routes traffic through AWS edge locations and the AWS backbone network, often improving throughput significantly for long-distance transfers:

aws s3api put-bucket-accelerate-configuration \
  --bucket my-bucket \
  --accelerate-configuration Status=Enabled

Then use the s3-accelerate endpoint in your SDK configuration.

Cache and Compress at the Edge

When serving content to end users, place CloudFront in front of S3. CloudFront caches objects at edge locations, reducing S3 request costs and improving latency. Enable compression in CloudFront to reduce transfer sizes for text-based assets like JSON, HTML, and CSS.

Putting It All Together

A well-architected S3 setup combines all three pillars. A practical baseline configuration for a new production bucket looks like this:

The following Terraform snippet captures this baseline:

resource "aws_s3_bucket" "app_data" {
  bucket = "my-app-data"
}

resource "aws_s3_bucket_public_access_block" "app_data" {
  bucket                  = aws_s3_bucket.app_data.id
  block_public_acls       = true
  ignore_public_acls      = true
  block_public_policy     = true
  restrict_public_buckets = true
}

resource "aws_s3_bucket_server_side_encryption_configuration" "app_data" {
  bucket = aws_s3_bucket.app_data.id
  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm     = "aws:kms"
      kms_master_key_id = aws_kms_key.s3.arn
    }
  }
}

resource "aws_s3_bucket_lifecycle_configuration" "app_data" {
  bucket = aws_s3_bucket.app_data.id
  rule {
    id     = "default"
    status = "Enabled"
    transition {
      days          = 30
      storage_class = "STANDARD_IA"
    }
    transition {
      days          = 90
      storage_class = "GLACIER"
    }
    noncurrent_version_expiration {
      noncurrent_days = 90
    }
  }
}

Conclusion

Amazon S3 rewards thoughtful configuration. By selecting the right storage classes and automating transitions with lifecycle rules, you keep costs predictable as data grows. By blocking public access, enforcing encryption, and applying least-privilege policies, you reduce the risk of costly security incidents. And by spreading keys, using multipart upload, and leveraging CloudFront, you ensure your applications stay fast even under heavy load. Treat these practices as a baseline for every new bucket, revisit them as your access patterns evolve, and your S3 footprint will remain secure, performant, and cost-efficient for the long term.

— Ad —

Google AdSense will appear here after approval

← Back to all articles