paulserban.eu

Writing Edition

Paul Serban

Writing, snippets & book notes

January 07, 2020

AWS Pricing Model Fundamentals: A Practical Guide for Engineers

How on-demand, reserved, spot, and savings-based pricing actually work - and how to design systems that don't bleed money

Introduction

Most engineers can spin up an EC2 instance, attach an S3 bucket, and wire up a Lambda function without thinking twice about cost. That's by design - AWS wants provisioning to feel frictionless. But the same simplicity that makes AWS easy to start with also makes it easy to misuse financially. A single misconfigured NAT Gateway, an over-provisioned RDS instance, or an unoptimized data transfer pattern can quietly turn a modest workload into a five-figure monthly bill.

Understanding AWS pricing isn't a finance function bolted onto engineering - it's a core part of system design. The same way you reason about latency, availability, and scalability, you need to reason about cost as a first-class architectural constraint. This article breaks down the mechanics of AWS pricing: how compute, storage, and networking are actually billed, the commitment models available to reduce cost, and the practical patterns experienced teams use to keep spend predictable without slowing down delivery.

This is not a marketing overview of "cloud economics." It's a technical walkthrough aimed at engineers who want to understand why their bill looks the way it does, and how to make deliberate, informed trade-offs when architecting systems on AWS.

Context: Why AWS Pricing Is Hard to Reason About

AWS pricing is complex for a structural reason: it bills based on granular, composable primitives rather than flat product prices. A single application might touch EC2 compute-hours, EBS volume IOPS, data transfer between availability zones, S3 request counts, and CloudWatch log ingestion - each metered independently, each with its own pricing dimension, and each governed by different discount mechanisms. There is no single "price of running my app." There's a sum of dozens of small, independently priced line items.

This granularity is deliberate. AWS's billing model mirrors its architectural philosophy: everything is a composable service, and you pay only for what you consume, at the resolution you consume it. The upside is that idle infrastructure - in theory - costs nothing. The downside is that cost visibility requires actively modeling your resource consumption, because the AWS console doesn't proactively tell you when an architecture pattern is expensive until the bill arrives at the end of the month.

A second source of complexity is that AWS pricing is regional and non-uniform. The same EC2 instance type costs different amounts in us-east-1 versus ap-southeast-2. Data transfer out to the internet is billed, but data transfer in generally is not. Data transfer between services in the same Availability Zone is often free, while transfer across Availability Zones within the same region is billed per gigabyte - a distinction that has real architectural implications for multi-AZ high-availability designs. None of this is discoverable by intuition; it has to be learned from the AWS Pricing documentation and reinforced through Cost Explorer analysis.

Deep Technical Explanation: The Core Pricing Models

AWS pricing fundamentally revolves around a small number of purchasing models that apply, with variations, across compute, database, and some analytics services. Understanding these models is the highest-leverage thing an engineer can do to control cloud spend.

On-Demand Pricing

On-demand is the default: you pay per second or per hour (depending on the service) for the resource you provision, with no upfront commitment and no long-term contract. This is the most flexible pricing model and the most expensive per unit of compute. It's appropriate for unpredictable workloads, early-stage development, spiky traffic, or any situation where the cost of over-committing outweighs the discount you'd get from committing.

The trap with on-demand pricing is that it scales linearly with resource count but not necessarily with business value. A development team that leaves ten m5.xlarge EC2 instances running 24/7 for a workload that's only used during business hours is paying full on-demand price for roughly 128 hours per week of actual use out of 168 available hours - nearly 24% waste before any other inefficiency is considered.

Reserved Instances and Savings Plans

Reserved Instances (RIs) and Savings Plans are AWS's mechanisms for trading commitment for discount. Both let you commit to either a specific instance configuration (RIs) or a consistent dollar-per-hour spend across compute usage (Savings Plans), in exchange for discounts that typically range from 30% to 72% compared to on-demand rates, depending on term length (one or three years) and payment structure (no upfront, partial upfront, or all upfront).

Savings Plans are generally the more flexible of the two: Compute Savings Plans apply across EC2 instance families, regions, and even to Fargate and Lambda usage, as long as your total compute spend meets the committed hourly rate. Reserved Instances, by contrast, are tied more tightly to instance family and region (with Convertible RIs offering some flexibility to exchange for different instance types). For most teams building on modern serverless-inclusive architectures, Savings Plans have become the preferred commitment vehicle because they don't require predicting the exact instance type you'll be running three years from now.

Spot Instances

Spot Instances let you bid for unused EC2 capacity at discounts of up to 90% relative to on-demand pricing. The trade-off is that AWS can reclaim Spot capacity with a two-minute warning when it's needed elsewhere. This makes Spot ideal for fault-tolerant, stateless, or checkpointable workloads: batch processing, CI/CD runners, distributed training jobs, and horizontally scaled stateless services behind a load balancer.

The engineering discipline required for Spot is different from on-demand or Reserved workloads. You need graceful interruption handling, idempotent job design, and often a mixed-instance strategy (via EC2 Auto Scaling Groups or Spot Fleet) that diversifies across instance types and Availability Zones to reduce the probability of simultaneous interruption across your fleet.

Implementation: Modeling and Monitoring Cost in Practice

Understanding pricing models conceptually is necessary but not sufficient - you need tooling and code-level practices that make cost visible during development, not just after the invoice arrives. AWS provides several APIs and services for this: Cost Explorer, the Cost and Usage Report (CUR), Budgets, and the Pricing API for programmatic cost estimation.

A common pattern for engineering teams is to build cost estimation into infrastructure-as-code review, rather than treating cost as something Finance discovers later. Below is a simplified example using the AWS SDK for JavaScript (v3) to query the Cost Explorer API and surface daily spend by service - a pattern useful for a Slack bot or CI check that flags anomalies before they compound over a billing cycle.

import {
  CostExplorerClient,
  GetCostAndUsageCommand,
  Granularity,
} from "@aws-sdk/client-cost-explorer";

const client = new CostExplorerClient({ region: "us-east-1" });

interface ServiceCost {
  service: string;
  amount: number;
}

async function getDailyCostByService(
  startDate: string,
  endDate: string
): Promise<ServiceCost[]> {
  const command = new GetCostAndUsageCommand({
    TimePeriod: { Start: startDate, End: endDate },
    Granularity: Granularity.DAILY,
    Metrics: ["UnblendedCost"],
    GroupBy: [{ Type: "DIMENSION", Key: "SERVICE" }],
  });

  const response = await client.send(command);
  const results: ServiceCost[] = [];

  for (const day of response.ResultsByTime ?? []) {
    for (const group of day.Groups ?? []) {
      const service = group.Keys?.[0] ?? "Unknown";
      const amount = parseFloat(
        group.Metrics?.UnblendedCost?.Amount ?? "0"
      );
      results.push({ service, amount });
    }
  }

  return results;
}

// Flag services whose spend jumped more than 25% day-over-day
function detectAnomalies(
  today: ServiceCost[],
  yesterday: ServiceCost[],
  thresholdPct = 25
): ServiceCost[] {
  const previous = new Map(yesterday.map((s) => [s.service, s.amount]));
  return today.filter((entry) => {
    const prevAmount = previous.get(entry.service) ?? 0;
    if (prevAmount === 0) return entry.amount > 5; // new spend above noise floor
    const changePct = ((entry.amount - prevAmount) / prevAmount) * 100;
    return changePct > thresholdPct;
  });
}

This kind of script, run daily via a scheduled Lambda function, turns cost monitoring from a monthly retrospective into a near-real-time signal. Paired with AWS Budgets alerts (which can trigger SNS notifications or even Lambda-based automated remediation), it closes the feedback loop between architectural decisions and their financial consequences.

Another practical implementation pattern is tagging discipline. Cost allocation tags - applied consistently to every resource via infrastructure-as-code (Terraform, CDK, or CloudFormation) - are what make Cost Explorer's grouping and filtering actually useful. Without tags like team, environment, and service, a bill with hundreds of resources becomes an undifferentiated number that's nearly impossible to attribute or optimize.

# Example: enforcing required tags in a CDK construct (Python)
from aws_cdk import Tags
from constructs import Construct

REQUIRED_TAGS = ["team", "environment", "cost-center"]

def apply_cost_tags(scope: Construct, tags: dict[str, str]) -> None:
    missing = [t for t in REQUIRED_TAGS if t not in tags]
    if missing:
        raise ValueError(f"Missing required cost allocation tags: {missing}")
    for key, value in tags.items():
        Tags.of(scope).add(key, value)

Enforcing this at the construct level, rather than relying on manual convention, prevents the slow drift toward untagged, unattributable spend that plagues most organizations after a year or two of active AWS usage.

Trade-offs and Common Pitfalls

Every pricing decision on AWS is a trade-off between flexibility and cost efficiency, and the pitfalls tend to cluster around three recurring mistakes.

The first is over-committing to Reserved Instances or Savings Plans before usage patterns are stable. Teams that commit to a three-year Savings Plan based on six weeks of production traffic often find themselves locked into a spend floor that doesn't match reality once the architecture evolves - for instance, after a migration to serverless compute or a shift to a different instance family for better price-performance. AWS does offer some flexibility (Convertible RIs, Savings Plan rate flexibility across compute types), but the fundamental commitment - a minimum hourly spend - doesn't disappear even if your actual usage drops.

The second pitfall is underestimating data transfer costs, which are notoriously easy to overlook during design but can dominate a bill at scale. Cross-AZ data transfer, NAT Gateway processing charges (billed per GB in addition to the hourly NAT Gateway charge itself), and data transfer out to the internet are common surprises. A frequently cited example in the AWS cost-optimization community is architectures that route inter-service traffic through a NAT Gateway unnecessarily, incurring both the hourly NAT charge and per-GB processing fees for traffic that could have stayed within a VPC endpoint or private subnet routing path.

The third pitfall is treating Spot Instances as a drop-in replacement for on-demand without re-architecting for interruption tolerance. Teams that naively move stateful, non-idempotent workloads to Spot to save money often end up with intermittent data corruption or failed jobs that cost more in engineering time and reprocessing than the Spot discount saved. Spot is a powerful tool, but it demands genuine architectural adaptation - checkpointing, idempotent retries, and diversified instance pools - not just a change in the EC2 launch configuration.

A subtler trade-off worth naming: pricing model choice interacts with organizational structure. Centralized FinOps teams that negotiate Enterprise Discount Programs or Private Pricing Agreements can extract additional discounts, but this requires cross-team cost visibility that many engineering organizations don't have. The technical and organizational sides of cost optimization are more tightly coupled than most teams initially assume.

Best Practices for Engineering Teams

A handful of practices consistently separate teams with predictable, well-managed AWS spend from those constantly surprised by their bill.

Start by right-sizing before committing. Use AWS Compute Optimizer or Cost Explorer's rightsizing recommendations to establish a stable baseline of actual resource utilization before purchasing Reserved Instances or Savings Plans. Committing to a discount on an over-provisioned baseline just locks in waste at a lower price - it doesn't eliminate it.

Layer your commitment strategy rather than treating it as all-or-nothing. Many mature AWS organizations run a mix: a Savings Plan covering the stable, predictable baseline load (perhaps 60-70% of steady-state usage), on-demand covering the unpredictable middle layer, and Spot covering elastic, fault-tolerant batch or scale-out workloads. This tiered approach captures most of the available discount without the risk of over-committing to a static instance profile.

Build cost checks into your deployment pipeline, not just your monitoring dashboard. Tools like Infracost can estimate the cost delta of a Terraform or CDK change before it's merged, giving engineers the same kind of pre-merge feedback for cost that they already get for test coverage or security scanning. This shifts cost awareness left, into the design and review process, rather than leaving it as a downstream surprise.

Finally, treat data transfer architecture as a design decision, not an afterthought. When designing multi-AZ or multi-region systems, explicitly model where data crosses billing boundaries - between AZs, between regions, or out to the internet - and use VPC endpoints, CloudFront, and same-AZ placement strategically to minimize unnecessary transfer charges. This is one of the highest-leverage areas because the savings compound with scale, and the fixes are usually architectural rather than requiring a new purchasing commitment.

Key Takeaways

Conclusion

AWS pricing isn't a separate concern from system design - it's an extension of it. Every architectural decision, from instance family selection to VPC topology to how you handle transient compute demand, has a direct and often underappreciated cost consequence. Engineers who treat pricing as a black box tend to build systems that work correctly but cost more than they should, sometimes by a wide margin.

The good news is that the underlying pricing models - on-demand, Reserved Instances, Savings Plans, and Spot - are a small, learnable set of primitives. Once you understand how they compose, and once you build the habit of checking cost implications the same way you'd check for a race condition or a missing index, cost optimization stops being a quarterly fire drill and becomes a natural part of how you design and ship systems. That shift in mindset, more than any single discount or tool, is what separates teams with predictable cloud spend from teams that dread opening their AWS invoice.

References

A quick flashcard preview - one example of each question type in this post. This is a demo of what you'll find in the full quiz app.

Flashcard preview - 1 / 9Multiple Choice

multiple choice - beginner - auto-graded

What is the primary trade-off of using On-Demand Pricing on AWS?

Choose an answer