Post

Practical Cost Optimization for AWS Data Pipelines

If you have been running data pipelines on AWS for a while, you have probably looked at the monthly bill and wondered where half of it came from. It is easy to spin up an EMR cluster, throw data into S3, and run Glue jobs without thinking about the cost, but over time small decisions add up.

In this article, let us look at practical ways to reduce data pipeline costs on AWS. These are things I have tried in real projects, not just theory from the docs. We will cover S3 storage, compute optimization, Glue-specific tuning, and how to set up cost monitoring that actually catches things before the bill arrives.

Start with S3: Where the Data Lives

For most data pipelines, S3 is the backbone. You store raw data, intermediate results, and final outputs there. The cost of S3 is not just the storage per GB, it is also the API calls (PUT, GET, LIST) and data transfer.

Lifecycle Policies Are the Lowest-Hanging Fruit

If your pipeline writes intermediate data that is only needed for a day or two, set up a lifecycle policy to delete it automatically. The same applies to old raw data that nobody queries anymore.

Here is an example lifecycle rule that moves objects to cheaper tiers over time and eventually deletes them:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
  "Rules": [
    {
      "Id": "move-to-IA-then-delete",
      "Status": "Enabled",
      "Transitions": [
        {
          "Days": 30,
          "StorageClass": "STANDARD_IA"
        },
        {
          "Days": 90,
          "StorageClass": "GLACIER"
        }
      ],
      "Expiration": {
        "Days": 365
      }
    }
  ]
}

For pipeline staging data that gets completely overwritten in every run, you can go straight to a 7-day or even 3-day expiration. You do not need to keep five thousand Parquet files from last month’s failed backfill run sitting in your staging prefix forever.

Intelligent Tiering vs Manual Rules

S3 Intelligent Tiering sounds great because it moves objects between access tiers automatically. But it comes with a per-object monitoring fee. If you have millions of small objects (looking at you, Spark job output with 200 tiny files per partition), the monitoring cost can cancel out the savings.

A rule of thumb I follow: use Intelligent Tiering for prefixes where access patterns actually vary and you cannot predict them. For data that predictably ages out (like raw event logs that get queried for a week and then sit idle), manual lifecycle rules with Standard → IA → Glacier are cheaper and simpler.

S3 Storage Class Comparison

Storage ClassBest ForRough Cost vs StandardRetrieval Cost
S3 StandardFrequently accessed, active pipeline dataBaselineNone
S3 Standard-IAAccessed once a month or less~40% cheaperPer GB retrieved
S3 One Zone-IAReproducible data, dev environments~55% cheaperPer GB retrieved
S3 Glacier FlexibleArchives, rarely need fast access~70% cheaperMinutes to hours
S3 Glacier Deep ArchiveCompliance, audit logs~85% cheaper12-48 hours

One thing I learned the hard way: do not use One Zone-IA for anything you cannot afford to lose. If that AZ goes down, your data is gone. It is fine for test pipeline outputs but not for production bronze-layer data that took three hours to backfill.

Compute: The Real Money Burner

Storage adds up slowly. Compute burns money fast, especially if jobs run longer than they need to because of poor configuration.

Spot Instances for EMR and Glue

If your EMR cluster uses On-Demand instances for everything, you are paying 60-70% more than you need to for the task nodes. Spot instances can handle interruptions gracefully in most data pipeline scenarios because EMR can request replacement nodes.

For EMR, a sensible setup is:

  • Master node: On-Demand only. Never spot. If the master goes down, the whole job dies.
  • Core nodes: On-Demand (these hold HDFS data if you use it).
  • Task nodes: Up to 80% Spot, with a fallback to On-Demand if Spot capacity is unavailable.

In Glue, you enable Spot by setting --enable-spot in the job parameters (available for Glue 3.0 and later). There is also a worker type called G.025X that is a quarter of a DPU and costs less than a quarter of the price, useful for lighter ETL jobs that do not need a full DPU.

1
2
# Glue job parameter to enable Spot
--enable-spot = true
1
2
3
4
5
6
7
8
9
10
# Boto3 example for creating a Glue job with Spot
response = glue.create_job(
    Name='my-cost-optimized-job',
    Role='GlueServiceRole',
    Command={'Name': 'glueetl', 'ScriptLocation': 's3://scripts/etl.py'},
    WorkerType='G.1X',
    NumberOfWorkers=5,
    ExecutionProperty={'MaxConcurrentRuns': 3}
)
# Spot is enabled via job parameters, not the API call itself

Right-Sizing Compute Resources

Glue jobs that read 10 GB of data do not need 100 workers. I have seen teams set NumberOfWorkers to 50 because “more workers means faster” and then discover the job spends more time provisioning workers than actually processing data.

A better approach: start with 2-5 workers, monitor the Spark UI, and scale up only if the job is bottlenecked. Use Glue’s auto-scaling feature, which adds and removes workers based on the workload. Combined with Spot instances, this is usually the cheapest way to run Glue jobs.

For EMR, use managed scaling instead of fixed instance counts. Managed scaling adjusts the cluster size based on YARN memory demands. Set a reasonable max and let it scale down during idle periods.

1
2
3
4
5
6
7
8
{
  "Name": "Managed scaling policy",
  "Description": "EMR managed scaling policy",
  "ScalingLimits": {
    "MinCapacityUnits": 2,
    "MaxCapacityUnits": 20
  }
}

One caveat with managed scaling on EMR: if your pipeline has a burst of short-lived tasks, the cluster might scale up just as the work finishes, leaving you with extra nodes for the remaining five minutes of runtime. It is not perfect, but it is better than paying for 20 nodes all day.

Data Transfer Costs Nobody Talks About

Moving data between AWS services in the same region is free. Moving data between regions costs money. Moving data out of AWS to the internet costs more money.

I once worked on a pipeline where the Glue job ran in us-east-1 but the source data was in an S3 bucket in eu-west-1. The monthly data transfer bill was higher than the Glue job cost itself. Keep your pipeline components in the same region unless you have a regulatory reason otherwise.

Also, if your pipeline uses the NAT Gateway for Glue jobs in a VPC with a private subnet, each GB processed through the NAT Gateway adds cost. For high-throughput pipelines, consider using VPC endpoints for S3 and other services instead.

Setting Up Cost Monitoring That Works

AWS Cost Explorer is useful for looking back at what happened last month, but it does not help you catch a runaway pipeline that started an hour ago.

Budget Alerts with Actual Thresholds

Create a monthly budget in AWS Budgets and set alerts at 50%, 80%, and 100% of your expected spend. But do not set a single $10,000 budget for everything. Break it down by service so you know which part of the pipeline is causing the spike.

1
2
3
4
5
# CLI example for creating a budget
aws budgets create-budget \
  --account-id 123456789012 \
  --budget "BudgetName=cost-optimization-blog-dpl, BudgetLimit={Amount=500,Unit=USD}, TimeUnit=MONTHLY, BudgetType=COST" \
  --notifications-with-subscribers file://notifications.json

Per-Pipeline Tagging

Tag every resource (EMR clusters, Glue jobs, S3 buckets) with at least Project and Environment tags. Without tags, the cost report is just a pile of numbers with no context. With tags, you can filter by project and see exactly which pipeline is the expensive one.

In Glue, tags are set on the job definition. For EMR, you can set tags at cluster creation. The key is enforcing it. If someone on the team creates resources manually, tags will get missed. Use a Service Control Policy (SCP) or a simple Lambda that checks for missing tags and alerts the team.

CloudWatch Alarms for Job Duration

Set up a CloudWatch alarm that triggers if a Glue job runs longer than, say, double its average duration. A job that normally takes 20 minutes suddenly taking 2 hours probably means something went wrong and you are paying for the privilege of watching it fail slowly.

1
2
3
4
5
6
7
8
9
10
11
# Pseudo-code for a CloudWatch alarm on Glue job duration
cloudwatch.put_metric_alarm(
    AlarmName='glue-job-timeout-alarm',
    MetricName='glue.driver.etl.job.elapsedTime',
    Namespace='Glue',
    Statistic='Maximum',
    Period=300,
    Threshold=3600,  # 1 hour in seconds
    ComparisonOperator='GreaterThanThreshold',
    AlarmActions=[sns_topic_arn]
)

Things to Be Careful About

There are a few gotchas that come up when trying to optimize costs:

  1. Spot interruptions are real. If your pipeline cannot handle losing a node mid-job (for example, a stateful streaming job), Spot is not for you. Stick to On-Demand or Reserved Instances.

  2. Compaction is not free. Small file compaction in Glue (running a job that reads many small files and writes fewer large files) costs compute. But the savings on S3 GET/LIST operations for downstream queries often outweigh it. Run the numbers for your access patterns.

  3. Athena costs sneak up. If you are using Athena to query pipeline outputs, remember you pay per TB scanned. Partition your data properly, use Parquet with compression, and consider running MSCK REPAIR TABLE less often (or switching to partition projection) to avoid scanning costs.

  4. Reserved Instances lock you in. For steady-state workloads (pipeline runs every hour, 24/7), Reserved Instances can save 30-40%. But if your workload pattern changes in six months, you are still paying. Start with On-Demand, profile your usage for a few months, then commit.

What Changes in Production

In a dev or testing environment, you can be aggressive with Spot instances and short retention periods. In production, you need to balance cost against reliability:

  • Keep a small On-Demand core for EMR instead of going all-Spot.
  • Retain intermediate data long enough to debug failures. Two weeks is usually enough.
  • Use separate S3 buckets or prefixes for prod and non-prod, with different lifecycle policies.
  • Set up anomaly detection in AWS Cost Explorer. It learns your normal spend patterns and alerts on unusual spikes without you having to define thresholds manually.

Cost optimization is not a one-time task. Your pipeline grows, data volumes change, AWS pricing changes, and something that was cheap six months ago might not be cheap now. Schedule a monthly 30-minute review of the pipeline costs, look at the tagged cost reports, and adjust.

The goal is not to spend zero. The goal is to know exactly what you are spending and why, so when someone asks about the AWS bill, you have an answer that is not just “it is complicated.”

This post is licensed under CC BY 4.0 by the author.