Skip to content
Ayhan Sipahi Ayhan Sipahi

AWS Cost Optimization Toolkit - Practical Strategies for Production Workloads

The order that makes AWS cost work stick: visibility with Cost Explorer and Budgets, right-sizing with Compute Optimizer, then commitments.

AWS cost work has a default order: build visibility, then right-size, then commit. Cost Explorer and Budgets show where the money goes, Compute Optimizer flags the capacity nobody uses, and Savings Plans or Reserved Instances discount whatever survives those two steps. Teams that reverse the order buy a discounted rate for capacity they should have deleted.

The symptoms that send teams looking are usually the same across production accounts: monthly bills that move without a matching traffic change, development resources running 24/7 when they are needed only during working hours, and instances billed for full capacity while their CPUs sit idle.

Five Recurring Cost Drivers

AWS ships capable native cost tooling, so a missing feature is rarely the blocker. The harder question is which tool answers which symptom, and in what sequence.

Five patterns show up in almost every production account:

  • Cost unpredictability: Monthly bills varying significantly without corresponding business growth
  • Idle resource waste: Non-production resources burning budget outside business hours
  • Over-provisioned instances: Paying for capacity that’s rarely utilized
  • Commitment paralysis: Difficulty choosing between Reserved Instances, Savings Plans, or Spot Instances
  • Lack of attribution: Unable to track which projects or teams drive AWS spending

Foundation: Cost Explorer and AWS Budgets

Before optimizing costs, you need visibility. Cost Explorer and AWS Budgets provide the foundation for understanding where money goes and catching anomalies early.

Cost Explorer Deep Dive

Cost Explorer offers 12-month historical data and up to 12 months forecasting. Here’s a practical implementation for analyzing cost trends and identifying anomalies:

import boto3
from datetime import datetime, timedelta

ce_client = boto3.client('ce', region_name='us-east-1')

def analyze_cost_trends(months_back=3):
    """
    Analyze cost trends across services and identify anomalies
    Returns services with >20% cost change month-over-month
    """
    end_date = datetime.now().date()
    start_date = (datetime.now() - timedelta(days=30 * months_back)).date()

    response = ce_client.get_cost_and_usage(
        TimePeriod={
            'Start': start_date.strftime('%Y-%m-%d'),
            'End': end_date.strftime('%Y-%m-%d')
        },
        Granularity='MONTHLY',
        Metrics=['UnblendedCost'],
        GroupBy=[
            {'Type': 'DIMENSION', 'Key': 'SERVICE'},
        ]
    )

    # Process results
    cost_by_service = {}
    for result in response['ResultsByTime']:
        period = result['TimePeriod']['Start']
        for group in result['Groups']:
            service = group['Keys'][0]
            cost = float(group['Metrics']['UnblendedCost']['Amount'])

            if service not in cost_by_service:
                cost_by_service[service] = []
            cost_by_service[service].append({
                'period': period,
                'cost': cost
            })

    # Identify services with >20% cost increase
    trending_services = []
    for service, costs in cost_by_service.items():
        if len(costs) >= 2:
            recent_cost = costs[-1]['cost']
            previous_cost = costs[-2]['cost']

            if previous_cost > 0:
                change_pct = ((recent_cost - previous_cost) / previous_cost) * 100

                if abs(change_pct) > 20:
                    trending_services.append({
                        'service': service,
                        'change': change_pct,
                        'current_cost': recent_cost,
                        'previous_cost': previous_cost
                    })

    return sorted(trending_services, key=lambda x: abs(x['change']), reverse=True)

This script identifies cost anomalies across AWS services. Running it weekly catches issues like misconfigured Auto Scaling groups or forgotten test resources before they accumulate significant costs.

Identifying Cost Allocation Gaps

The cheapest optimization is often finding out what nobody is tracking. Untagged spend hides inside the total, and no tag-filtered dashboard will surface it for you:

def analyze_cost_allocation_gaps():
    """
    Identify costs that aren't properly tagged for cost allocation
    """
    end_date = datetime.now().date()
    start_date = (datetime.now() - timedelta(days=30)).date()

    # Check costs by tag
    tagged_response = ce_client.get_cost_and_usage(
        TimePeriod={
            'Start': start_date.strftime('%Y-%m-%d'),
            'End': end_date.strftime('%Y-%m-%d')
        },
        Granularity='MONTHLY',
        Metrics=['UnblendedCost'],
        GroupBy=[
            {'Type': 'TAG', 'Key': 'Project'},
        ]
    )

    total_tagged_cost = 0
    for result in tagged_response['ResultsByTime']:
        for group in result['Groups']:
            if group['Keys'][0]:  # Has project tag
                total_tagged_cost += float(group['Metrics']['UnblendedCost']['Amount'])

    # Get total cost
    total_response = ce_client.get_cost_and_usage(
        TimePeriod={
            'Start': start_date.strftime('%Y-%m-%d'),
            'End': end_date.strftime('%Y-%m-%d')
        },
        Granularity='MONTHLY',
        Metrics=['UnblendedCost']
    )

    total_cost = float(total_response['ResultsByTime'][0]['Total']['UnblendedCost']['Amount'])
    untagged_cost = total_cost - total_tagged_cost
    untagged_percentage = (untagged_cost / total_cost) * 100

    return {
        'total_cost': total_cost,
        'tagged_cost': total_tagged_cost,
        'untagged_cost': untagged_cost,
        'untagged_percentage': untagged_percentage
    }

Activate cost allocation tags in the Billing Console before you rely on them. Tags only track costs after activation; there is no retroactive tagging capability.

AWS Budgets with Automated Actions

Budgets provide proactive cost monitoring. Here’s a production-ready implementation with multiple alert thresholds:

import boto3

budgets_client = boto3.client('budgets')

def create_department_budget_with_alerts(
    account_id: str,
    department: str,
    monthly_limit: float
):
    """
    Create budget with multiple alert thresholds and SNS notifications
    70% = Info, 90% = Warning, 100% = Critical, Forecasted = Predictive
    """
    budget_name = f"{department}-monthly-budget"

    budgets_client.create_budget(
        AccountId=account_id,
        Budget={
            'BudgetName': budget_name,
            'BudgetLimit': {
                'Amount': str(monthly_limit),
                'Unit': 'USD'
            },
            'TimeUnit': 'MONTHLY',
            'BudgetType': 'COST',
            'CostFilters': {
                'TagKeyValue': [f'user:Department${department}']
            }
        },
        NotificationsWithSubscribers=[
            # 70% threshold - Info alert
            {
                'Notification': {
                    'NotificationType': 'ACTUAL',
                    'ComparisonOperator': 'GREATER_THAN',
                    'Threshold': 70.0,
                    'ThresholdType': 'PERCENTAGE',
                    'NotificationState': 'ALARM'
                },
                'Subscribers': [
                    {
                        'SubscriptionType': 'SNS',
                        'Address': f'arn:aws:sns:us-east-1:{account_id}:budget-alerts-info'
                    }
                ]
            },
            # 90% threshold - Warning alert
            {
                'Notification': {
                    'NotificationType': 'ACTUAL',
                    'ComparisonOperator': 'GREATER_THAN',
                    'Threshold': 90.0,
                    'ThresholdType': 'PERCENTAGE',
                    'NotificationState': 'ALARM'
                },
                'Subscribers': [
                    {
                        'SubscriptionType': 'SNS',
                        'Address': f'arn:aws:sns:us-east-1:{account_id}:budget-alerts-warning'
                    }
                ]
            },
            # 100% threshold - Critical alert
            {
                'Notification': {
                    'NotificationType': 'ACTUAL',
                    'ComparisonOperator': 'GREATER_THAN',
                    'Threshold': 100.0,
                    'ThresholdType': 'PERCENTAGE',
                    'NotificationState': 'ALARM'
                },
                'Subscribers': [
                    {
                        'SubscriptionType': 'SNS',
                        'Address': f'arn:aws:sns:us-east-1:{account_id}:budget-alerts-critical'
                    },
                    {
                        'SubscriptionType': 'EMAIL',
                        'Address': f'{department}[email protected]'
                    }
                ]
            },
            # Forecasted to exceed - Predictive alert
            {
                'Notification': {
                    'NotificationType': 'FORECASTED',
                    'ComparisonOperator': 'GREATER_THAN',
                    'Threshold': 100.0,
                    'ThresholdType': 'PERCENTAGE',
                    'NotificationState': 'ALARM'
                },
                'Subscribers': [
                    {
                        'SubscriptionType': 'SNS',
                        'Address': f'arn:aws:sns:us-east-1:{account_id}:budget-forecast-alerts'
                    }
                ]
            }
        ]
    )

    print(f"Created budget {budget_name} with ${monthly_limit} monthly limit")

Budget thresholds are evaluated roughly three times a day, which catches an anomaly well before a daily email digest would. FORECASTED alerts use AWS’s prediction model and need at least 5 weeks of historical data before they produce anything.

Common pitfalls to avoid:

  • Creating too many budgets causes alert fatigue; focus on key cost centers
  • Using FORECASTED alerts without understanding they need 5+ weeks of historical data
  • Not activating cost allocation tags before filtering budgets by tags
  • Ignoring untagged resource costs; they stay invisible in every tag-filtered report

Right-Sizing with Compute Optimizer

AWS Compute Optimizer uses machine learning to analyze CloudWatch metrics and recommend optimal instance types, Lambda memory configurations, and EBS volumes. This typically delivers 20-40% cost savings with low implementation risk.

Automated Right-Sizing Implementation

import boto3
from typing import List, Dict
from dataclasses import dataclass

compute_optimizer = boto3.client('compute-optimizer')
ec2_client = boto3.client('ec2')

@dataclass
class RightsizingRecommendation:
    instance_id: str
    current_type: str
    recommended_type: str
    current_cost_monthly: float
    recommended_cost_monthly: float
    savings_monthly: float
    cpu_utilization_avg: float
    memory_utilization_avg: float

def get_underutilized_instances(
    max_cpu_threshold: float = 40.0,
    lookback_days: int = 14
) -> List[RightsizingRecommendation]:
    """
    Fetch Compute Optimizer recommendations for underutilized instances
    Default: instances averaging <40% CPU over 14 days
    """
    recommendations = []

    # Get EC2 instance recommendations
    paginator = compute_optimizer.get_paginator('get_ec2_instance_recommendations')

    for page in paginator.paginate():
        for rec in page.get('instanceRecommendations', []):
            instance_id = rec['instanceArn'].split('/')[-1]
            current_instance_type = rec['currentInstanceType']

            # Get utilization metrics
            cpu_util = next(
                (m['value'] for m in rec.get('utilizationMetrics', [])
                 if m['name'] == 'CPU'),
                0.0
            )

            memory_util = next(
                (m['value'] for m in rec.get('utilizationMetrics', [])
                 if m['name'] == 'MEMORY'),
                0.0
            )

            # Check if instance is underutilized
            if cpu_util < max_cpu_threshold:
                # Get best recommendation option
                if rec.get('recommendationOptions'):
                    best_option = rec['recommendationOptions'][0]

                    # Calculate savings
                    estimated_savings = best_option.get('estimatedMonthlySavings', {}).get('value', 0)

                    recommendations.append(RightsizingRecommendation(
                        instance_id=instance_id,
                        current_type=current_instance_type,
                        recommended_type=best_option['instanceType'],
                        current_cost_monthly=estimated_savings,
                        recommended_cost_monthly=0,  # Would calculate from pricing API
                        savings_monthly=estimated_savings,
                        cpu_utilization_avg=cpu_util,
                        memory_utilization_avg=memory_util
                    ))

    # Sort by potential savings (highest first)
    return sorted(recommendations, key=lambda x: x.savings_monthly, reverse=True)

Applying Rightsizing Recommendations

Here’s a cautious approach to applying recommendations; instances must be stopped first:

def apply_rightsizing_recommendation(
    instance_id: str,
    new_instance_type: str,
    dry_run: bool = True
) -> Dict:
    """
    Apply right-sizing recommendation by modifying instance type
    Requires instance to be stopped first
    """
    try:
        # Get current instance state
        response = ec2_client.describe_instances(InstanceIds=[instance_id])
        instance_state = response['Reservations'][0]['Instances'][0]['State']['Name']

        if instance_state != 'stopped':
            return {
                'success': False,
                'message': f'Instance must be stopped. Current state: {instance_state}'
            }

        # Modify instance type
        ec2_client.modify_instance_attribute(
            InstanceId=instance_id,
            InstanceType={'Value': new_instance_type},
            DryRun=dry_run
        )

        return {
            'success': True,
            'message': f'Successfully modified {instance_id} to {new_instance_type}',
            'instance_id': instance_id,
            'new_type': new_instance_type
        }

    except Exception as e:
        return {
            'success': False,
            'message': f'Failed to modify instance: {str(e)}'
        }

Rolling these out safely matters more than rolling them out fast. Test in development first, then apply to 10% of production instances, monitor for a week, and widen from there.

Lambda Memory Optimization

Compute Optimizer also analyzes Lambda functions. Here’s how to get Lambda-specific recommendations:

def get_lambda_optimization_recommendations():
    """
    Get Lambda memory configuration recommendations
    Returns functions where memory can be reduced without performance impact
    """
    paginator = compute_optimizer.get_paginator('get_lambda_function_recommendations')

    recommendations = []

    for page in paginator.paginate():
        for rec in page.get('lambdaFunctionRecommendations', []):
            function_arn = rec['functionArn']
            current_memory = rec['currentMemorySize']

            if rec.get('recommendationOptions'):
                best_option = rec['recommendationOptions'][0]
                recommended_memory = best_option['memorySize']

                # Only include if recommendation differs from current
                if recommended_memory != current_memory:
                    estimated_savings = best_option.get('estimatedMonthlySavings', {})

                    recommendations.append({
                        'function_arn': function_arn,
                        'function_name': function_arn.split(':')[-1],
                        'current_memory_mb': current_memory,
                        'recommended_memory_mb': recommended_memory,
                        'savings_monthly': estimated_savings.get('value', 0),
                        'savings_currency': estimated_savings.get('currency', 'USD')
                    })

    return sorted(recommendations, key=lambda x: x['savings_monthly'], reverse=True)

Compute Optimizer analyzes CloudWatch metrics from the last 14 days by default. For production workloads with monthly usage patterns, enable enhanced infrastructure metrics ($0.0003360215 per resource per hour, adds ~$0.25/month per resource) to get a 93-day lookback period.

Common pitfalls with Compute Optimizer:

  • Applying recommendations during business hours without a maintenance window
  • Not testing recommended instance types for compatibility (some types don’t support all features)
  • Ignoring “Under-provisioned” warnings; cost savings shouldn’t compromise performance
  • Not enabling enhanced metrics for production workloads; 14 days may miss monthly spikes

Commitment Strategy: Savings Plans vs Reserved Instances

Choosing between Reserved Instances, Savings Plans, or staying on-demand requires understanding workload characteristics. Here’s a decision framework:

No

Yes

Highly Predictable Same Instance Family

Flexible Multiple Services

EC2 Only May Change Families

Workload Analysis

Workload Stable for 1-3 Years?

Use On-Demand or Spot Instances

Usage Pattern Predictable?

Reserved Instances Up to 72% Savings

Compute Savings Plans Up to 66% Savings

EC2 Instance Savings Plans Up to 72% Savings

Payment Option?

All Upfront Maximum Discount

Partial Upfront Balanced

No Upfront Minimum Discount

Recommendations Engine

Here’s how to programmatically get AWS’s commitment recommendations:

import boto3
from typing import List, Dict
from dataclasses import dataclass

ce_client = boto3.client('ce')

@dataclass
class CommitmentRecommendation:
    recommendation_type: str  # 'RI' or 'SavingsPlan'
    service: str
    term: str  # '1_YEAR' or '3_YEAR'
    payment_option: str
    monthly_commitment: float
    estimated_savings: float
    estimated_savings_percentage: float

def get_savings_plan_recommendations(
    term_years: int = 1,
    payment_option: str = 'NO_UPFRONT'
) -> List[CommitmentRecommendation]:
    """
    Get Savings Plans purchase recommendations
    Based on last 30 days of usage patterns
    """
    response = ce_client.get_savings_plans_purchase_recommendation(
        SavingsPlansType='COMPUTE_SP',  # or 'EC2_INSTANCE_SP'
        TermInYears=f'{term_years}_YEAR',
        PaymentOption=payment_option,
        LookbackPeriodInDays='THIRTY_DAYS',
        AccountScope='PAYER'
    )

    recommendations = []

    for rec in response.get('SavingsPlansPurchaseRecommendation', {}).get('SavingsPlansPurchaseRecommendationDetails', []):
        savings_details = rec.get('SavingsPlansDetails', {})

        recommendations.append(CommitmentRecommendation(
            recommendation_type='SavingsPlan',
            service='Compute',
            term=f'{term_years}_YEAR',
            payment_option=payment_option,
            monthly_commitment=float(rec.get('HourlyCommitmentToPurchase', 0)) * 730,
            estimated_savings=float(rec.get('EstimatedMonthlySavingsAmount', 0)),
            estimated_savings_percentage=float(rec.get('EstimatedSavingsPercentage', 0))
        ))

    return recommendations

Comparison Framework

For workloads with varying stability, here’s a comparison engine:

def compare_commitment_options(
    monthly_spend: float,
    workload_stability: str  # 'stable', 'variable', 'mixed'
) -> Dict:
    """
    Compare commitment strategies based on workload characteristics
    """
    if workload_stability == 'stable':
        # Reserved Instances for predictable workloads
        return {
            'recommendation': 'Reserved Instances',
            'reason': 'Highest discount for stable, predictable workloads',
            'expected_savings': monthly_spend * 0.72,  # Up to 72% with a 3-year RI
            'flexibility': 'Low - locked to instance family and region',
            'best_for': 'Production databases, always-on services'
        }

    elif workload_stability == 'variable':
        # Compute Savings Plans for flexibility
        return {
            'recommendation': 'Compute Savings Plans',
            'reason': 'Flexibility across instance families, regions, and compute services',
            'expected_savings': monthly_spend * 0.66,  # Up to 66% savings
            'flexibility': 'High - applies to EC2, Fargate, Lambda',
            'best_for': 'Multi-service architectures, evolving workloads'
        }

    else:  # mixed
        # Hybrid approach
        stable_portion = monthly_spend * 0.6  # 60% stable baseline
        variable_portion = monthly_spend * 0.4  # 40% variable

        return {
            'recommendation': 'Hybrid Strategy',
            'reason': 'Combine RIs for baseline, Savings Plans for flexibility',
            'breakdown': {
                'reserved_instances': {
                    'monthly_commitment': stable_portion,
                    'savings': stable_portion * 0.72
                },
                'savings_plans': {
                    'monthly_commitment': variable_portion,
                    'savings': variable_portion * 0.66
                }
            },
            'expected_total_savings': (stable_portion * 0.72) + (variable_portion * 0.66),
            'flexibility': 'Balanced - optimized for both scenarios'
        }

How the three commitment types differ:

  • Reserved Instances: Up to 72% savings, but locked to specific instance family and region. Can sell on RI Marketplace if needs change.
  • Compute Savings Plans: Up to 66% savings, applies across EC2, Fargate, and Lambda in any region or instance family. Maximum flexibility.
  • EC2 Instance Savings Plans: Up to 72% savings, flexible within instance family and region. Middle ground between RIs and Compute SPs.
  • Payment options: All Upfront (highest discount), Partial Upfront (balanced), No Upfront (lowest discount but no capital commitment)

Savings Plans are no longer strictly one-way. AWS offers a 7-day return window with restrictions (hourly commitment $100 or less, returns within the same calendar month, maximum 10 returns per year), which takes some of the fear out of a first purchase.

Common pitfalls with commitments:

  • Over-committing based on peak usage instead of baseline; results in unused commitments
  • Choosing 3-year terms without considering technology evolution; instance types improve rapidly
  • Not monitoring RI/SP utilization after purchase; underutilized commitments waste money
  • Mixing RIs and SPs without clear strategy; can lead to coverage gaps or overlaps

A conservative ramp keeps the mistakes cheap: cover 40% of baseline usage in month 1, increase to 60% if utilization exceeds 95%, and target 70-80% coverage long-term. The remaining 20-30% stays on-demand for flexibility and growth.

Spot Instances for Batch Workloads

Spot Instances offer 70-90% cost savings but require interruption-resilient architecture. Here’s when and how to use them effectively:

No

Yes

Short Tasks Less than 2 hours

Long Running Need Persistence

Workload Classification

Interruption Tolerant?

Use On-Demand or Reserved Instances

Expected Duration?

Spot Instances 70-90% Savings

Spot with Checkpointing

Instance Diversification

Multiple Instance Types Multiple AZs

Capacity-Optimized Allocation Strategy

2-Minute Warning Interruption Handler

Spot Fleet with Diversification

The key to Spot Instance resilience is diversification across instance types and availability zones:

import boto3
from typing import List, Dict

ec2_client = boto3.client('ec2')
autoscaling_client = boto3.client('autoscaling')

def create_diversified_spot_fleet(
    target_capacity: int,
    instance_types: List[str],
    subnets: List[str],
    user_data_script: str
) -> str:
    """
    Create EC2 Auto Scaling group with diversified Spot instances
    Uses capacity-optimized allocation strategy to minimize interruptions
    """
    # Launch template configuration
    launch_template_overrides = []

    for instance_type in instance_types:
        for subnet in subnets:
            launch_template_overrides.append({
                'InstanceType': instance_type,
                'SubnetId': subnet,
                'WeightedCapacity': 1.0
            })

    # Create Auto Scaling group with mixed instances policy
    asg_config = {
        'AutoScalingGroupName': 'spot-optimized-asg',
        'MinSize': target_capacity,
        'MaxSize': target_capacity * 2,
        'DesiredCapacity': target_capacity,
        'VPCZoneIdentifier': ','.join(subnets),
        'MixedInstancesPolicy': {
            'InstancesDistribution': {
                'OnDemandBaseCapacity': 0,  # All Spot instances
                'OnDemandPercentageAboveBaseCapacity': 0,
                'SpotAllocationStrategy': 'capacity-optimized',
                'SpotInstancePools': len(instance_types) * len(subnets)
            },
            'LaunchTemplate': {
                'LaunchTemplateSpecification': {
                    'LaunchTemplateName': 'spot-fleet-template',
                    'Version': '$Latest'
                },
                'Overrides': launch_template_overrides
            }
        },
        'Tags': [
            {
                'Key': 'CostOptimization',
                'Value': 'SpotInstances',
                'PropagateAtLaunch': True
            }
        ]
    }

    response = autoscaling_client.create_auto_scaling_group(**asg_config)

    return asg_config['AutoScalingGroupName']

Diversification is the whole game here. Use the capacity-optimized allocation strategy and spread across 10+ instance types and 3+ availability zones. A fleet pinned to a single instance type competes for a single pool, so it gets reclaimed every time that pool tightens.

Interruption Handling

Spot Instances provide a 2-minute warning via EventBridge before termination. Here’s a Lambda function to handle graceful shutdowns:

# Lambda function for Spot interruption handling
import boto3
import json

ec2_client = boto3.client('ec2')
sqs_client = boto3.client('sqs')

def lambda_handler(event, context):
    '''
    Handle EC2 Spot Instance interruption warnings (2-minute notice)
    Strategy: Drain tasks and return work to queue
    '''
    # Parse EventBridge event
    detail = event.get('detail', {})
    instance_id = detail.get('instance-id')
    instance_action = detail.get('instance-action')  # 'terminate', 'stop', or 'hibernate'

    if not instance_id:
        return {'statusCode': 400, 'body': 'No instance ID in event'}

    print(f'Spot interruption warning for {instance_id}: {instance_action}')

    # Get instance details
    response = ec2_client.describe_instances(InstanceIds=[instance_id])
    instance = response['Reservations'][0]['Instances'][0]

    # Check for tasks queue
    task_queue_url = get_tag_value(instance.get('Tags', []), 'TaskQueueUrl')

    if task_queue_url:
        # Return in-progress tasks to queue
        sqs_client.send_message(
            QueueUrl=task_queue_url,
            MessageBody=json.dumps({
                'action': 'drain_instance',
                'instance_id': instance_id,
                'interruption_time': detail.get('time')
            })
        )

    # Auto Scaling group automatically replaces terminated Spot instances
    # with capacity-optimized strategy

    return {
        'statusCode': 200,
        'body': f'Handled interruption for {instance_id}'
    }

def get_tag_value(tags: list, key: str) -> str:
    for tag in tags:
        if tag['Key'] == key:
            return tag['Value']
    return None

Checkpointing for Long-Running Jobs

For jobs longer than 2 hours, implement checkpointing to resume from interruptions:

import pickle
import boto3
from dataclasses import dataclass
from typing import Any

s3_client = boto3.client('s3')

@dataclass
class CheckpointState:
    job_id: str
    progress_percentage: float
    current_step: int
    total_steps: int
    intermediate_results: Any
    timestamp: str

def save_checkpoint(
    state: CheckpointState,
    bucket: str,
    prefix: str = 'checkpoints'
):
    """
    Save job checkpoint to S3 for recovery after Spot interruption
    """
    checkpoint_key = f"{prefix}/{state.job_id}/checkpoint-{state.current_step}.pkl"

    # Serialize state
    checkpoint_data = pickle.dumps(state)

    # Upload to S3
    s3_client.put_object(
        Bucket=bucket,
        Key=checkpoint_key,
        Body=checkpoint_data,
        ServerSideEncryption='AES256'
    )

    print(f"Checkpoint saved: {checkpoint_key} ({state.progress_percentage:.1f}% complete)")

def restore_checkpoint(
    job_id: str,
    bucket: str,
    prefix: str = 'checkpoints'
) -> CheckpointState:
    """
    Restore latest checkpoint for interrupted job
    """
    # List all checkpoints for this job
    response = s3_client.list_objects_v2(
        Bucket=bucket,
        Prefix=f"{prefix}/{job_id}/"
    )

    if not response.get('Contents'):
        raise ValueError(f"No checkpoints found for job {job_id}")

    # Get latest checkpoint
    latest_checkpoint = sorted(
        response['Contents'],
        key=lambda x: x['LastModified'],
        reverse=True
    )[0]

    # Download and deserialize
    checkpoint_obj = s3_client.get_object(
        Bucket=bucket,
        Key=latest_checkpoint['Key']
    )

    state = pickle.loads(checkpoint_obj['Body'].read())

    print(f"Restored checkpoint from {state.timestamp} ({state.progress_percentage:.1f}% complete)")

    return state

Where Spot fits:

  • Good fit: batch processing, CI/CD runners, ML training, data analysis, containerized workloads
  • Poor fit: user-facing applications without a fallback, stateful applications without checkpointing
  • Diversification: pick instance types with similar CPU/memory ratios (m5, m5a, m5n, m6i, m6a for general compute)
  • Checkpoint frequency: every 5-10 minutes for jobs longer than 30 minutes

S3 Storage Optimization

S3 storage costs can be reduced by 40-95% through Intelligent-Tiering and lifecycle policies. Here’s how to implement it effectively:

import boto3
from typing import List, Dict

s3_client = boto3.client('s3')

def create_intelligent_lifecycle_policy(
    bucket_name: str,
    prefix: str = '',
    enable_deep_archive_tier: bool = True
) -> Dict:
    """
    Create S3 lifecycle policy to transition objects to Intelligent-Tiering
    with optional deep archive tiers for rarely accessed data
    """
    lifecycle_rules = []

    # Rule 1: Transition to Intelligent-Tiering immediately
    intelligent_tiering_rule = {
        'ID': 'transition-to-intelligent-tiering',
        'Filter': {'Prefix': prefix},
        'Status': 'Enabled',
        'Transitions': [
            {
                'Days': 0,  # Immediate transition
                'StorageClass': 'INTELLIGENT_TIERING'
            }
        ]
    }

    lifecycle_rules.append(intelligent_tiering_rule)

    # Rule 2: Delete incomplete multipart uploads after 7 days
    multipart_cleanup_rule = {
        'ID': 'cleanup-incomplete-multipart-uploads',
        'Filter': {'Prefix': prefix},
        'Status': 'Enabled',
        'AbortIncompleteMultipartUpload': {
            'DaysAfterInitiation': 7
        }
    }

    lifecycle_rules.append(multipart_cleanup_rule)

    # Rule 3: Delete old versions after 90 days (if versioning enabled)
    noncurrent_version_rule = {
        'ID': 'expire-old-versions',
        'Filter': {'Prefix': prefix},
        'Status': 'Enabled',
        'NoncurrentVersionExpiration': {
            'NoncurrentDays': 90
        }
    }

    lifecycle_rules.append(noncurrent_version_rule)

    # Apply lifecycle configuration
    s3_client.put_bucket_lifecycle_configuration(
        Bucket=bucket_name,
        LifecycleConfiguration={
            'Rules': lifecycle_rules
        }
    )

    # Enable Intelligent-Tiering archive configurations
    if enable_deep_archive_tier:
        s3_client.put_bucket_intelligent_tiering_configuration(
            Bucket=bucket_name,
            Id='deep-archive-config',
            IntelligentTieringConfiguration={
                'Id': 'deep-archive-config',
                'Status': 'Enabled',
                'Tierings': [
                    {
                        'Days': 90,
                        'AccessTier': 'ARCHIVE_ACCESS'  # After 90 days no access
                    },
                    {
                        'Days': 180,
                        'AccessTier': 'DEEP_ARCHIVE_ACCESS'  # After 180 days
                    }
                ]
            }
        )

    return {
        'bucket': bucket_name,
        'rules_applied': len(lifecycle_rules),
        'intelligent_tiering_enabled': True,
        'deep_archive_enabled': enable_deep_archive_tier
    }

What Intelligent-Tiering actually does:

  • Automatic access tiers: Frequent Access, Infrequent Access (30 days), Archive Instant Access (90 days), optional Archive Access (90 days), optional Deep Archive Access (180 days)
  • Cost savings: up to 68% with Archive Instant Access, up to 95% with Deep Archive
  • Monitoring fee: $0.0025 per 1,000 objects, which is negligible for large objects and dominant for tiny ones
  • Minimum object size: 128KB; smaller objects stay in the Frequent Access tier and still pay the monitoring fee
  • No retrieval fees for the Frequent, Infrequent, or Archive Instant Access tiers

Lambda Cost Optimization

Lambda costs comprise three components: requests, duration (GB-seconds), and optional provisioned concurrency. Here’s how to optimize each:

Memory Optimization with Power Tuning

AWS Lambda Power Tuning (open-source) provides data-driven memory optimization:

import boto3
import json
from typing import Dict, List

stepfunctions_client = boto3.client('stepfunctions')
lambda_client = boto3.client('lambda')

def run_lambda_power_tuning(
    function_name: str,
    power_values: List[int] = [128, 256, 512, 1024, 1536, 2048, 3008],
    num_invocations: int = 10,
    strategy: str = 'balanced'  # 'cost', 'speed', or 'balanced'
) -> Dict:
    """
    Run AWS Lambda Power Tuning to find optimal memory configuration
    Uses open-source Step Functions state machine
    GitHub: alexcasalboni/aws-lambda-power-tuning
    """
    state_machine_arn = 'arn:aws:states:us-east-1:123456789012:stateMachine:lambda-power-tuner'

    # Input payload for power tuning
    input_payload = {
        'lambdaARN': f'arn:aws:lambda:us-east-1:123456789012:function:{function_name}',
        'powerValues': power_values,
        'num': num_invocations,
        'payload': {},  # Your test payload
        'parallelInvocation': True,
        'strategy': strategy
    }

    # Execute power tuning
    response = stepfunctions_client.start_execution(
        stateMachineArn=state_machine_arn,
        input=json.dumps(input_payload)
    )

    execution_arn = response['executionArn']

    print(f"Power tuning execution started: {execution_arn}")
    print(f"Strategy: {strategy} | Memory configs: {power_values}")

    # Wait for execution to complete
    waiter = stepfunctions_client.get_waiter('execution_succeeded')
    waiter.wait(executionArn=execution_arn)

    # Get results
    result_response = stepfunctions_client.describe_execution(
        executionArn=execution_arn
    )

    output = json.loads(result_response['output'])

    return {
        'recommended_memory': output.get('power'),
        'optimization_summary': output
    }

Provisioned Concurrency Cost Analysis

Provisioned concurrency eliminates cold starts, at roughly $11 per month for every GB kept warm. Here’s when that trade is worth making:

from dataclasses import dataclass
from typing import Optional

@dataclass
class LambdaCostBreakdown:
    invocations_monthly: int
    avg_duration_ms: int
    memory_mb: int
    provisioned_concurrency: Optional[int] = None

def calculate_lambda_costs(config: LambdaCostBreakdown) -> Dict:
    """
    Calculate Lambda costs with and without provisioned concurrency
    Helps decide if provisioned concurrency is cost-effective
    """
    # Pricing (US East N. Virginia)
    price_per_request = 0.20 / 1_000_000  # $0.20 per 1M requests
    price_per_gb_second = 0.0000166667
    provisioned_price_per_gb_second = 0.0000041667

    # Convert memory to GB
    memory_gb = config.memory_mb / 1024

    # Convert duration to seconds
    duration_seconds = config.avg_duration_ms / 1000

    # Standard (on-demand) cost
    request_cost = config.invocations_monthly * price_per_request
    compute_cost = (
        config.invocations_monthly *
        duration_seconds *
        memory_gb *
        price_per_gb_second
    )

    total_on_demand = request_cost + compute_cost

    # Provisioned concurrency cost (if applicable)
    provisioned_cost = 0
    if config.provisioned_concurrency:
        seconds_per_month = 730 * 3600
        provisioned_cost = (
            config.provisioned_concurrency *
            memory_gb *
            seconds_per_month *
            provisioned_price_per_gb_second
        )

    total_with_provisioned = request_cost + compute_cost + provisioned_cost

    return {
        'on_demand_cost_monthly': round(total_on_demand, 2),
        'provisioned_cost_monthly': round(total_with_provisioned, 2),
        'cost_difference': round(total_with_provisioned - total_on_demand, 2),
        'breakdown': {
            'request_charges': round(request_cost, 2),
            'compute_charges': round(compute_cost, 2),
            'provisioned_charges': round(provisioned_cost, 2)
        },
        # 1.5x is a starting threshold for "the latency is worth the premium";
        # set it from your own latency budget
        'recommendation': 'Use provisioned concurrency' if (
            config.provisioned_concurrency and
            total_with_provisioned < total_on_demand * 1.5
        ) else 'Stick with on-demand'
    }

# Example: Interactive API
interactive_api = LambdaCostBreakdown(
    invocations_monthly=5_000_000,
    avg_duration_ms=200,
    memory_mb=1024,
    provisioned_concurrency=10
)

costs = calculate_lambda_costs(interactive_api)
print(f"On-demand: ${costs['on_demand_cost_monthly']}")
print(f"With provisioned: ${costs['provisioned_cost_monthly']}")
print(f"Recommendation: {costs['recommendation']}")

What actually moves the Lambda bill:

  • Memory allocation also determines CPU and network share, so more memory often means shorter duration and a lower total
  • 1024-1536MB tends to be the practical range for a cost/performance balance, but measure before settling
  • Compute Savings Plans apply to Lambda duration charges (up to 17% discount)
  • Reducing the code’s own runtime usually beats memory tuning, and it costs nothing per month
  • Provisioned concurrency belongs on user-facing APIs with a strict latency budget, not on sporadic workloads

Cost Allocation and Tagging

Implementing comprehensive tagging enables cost attribution across teams, projects, and environments. Here’s a production-ready approach:

import boto3
from typing import Dict, List

organizations_client = boto3.client('organizations')
config_client = boto3.client('config')

def create_tagging_compliance_rule():
    """
    Create AWS Config rule to detect untagged resources
    Enforces Environment, Project, CostCenter tags
    """
    config_rule = {
        'ConfigRuleName': 'required-tags-compliance',
        'Description': 'Check that resources have required cost allocation tags',
        'Source': {
            'Owner': 'AWS',
            'SourceIdentifier': 'REQUIRED_TAGS'
        },
        'InputParameters': '{"tag1Key":"Environment","tag2Key":"Project","tag3Key":"CostCenter"}',
        'Scope': {
            'ComplianceResourceTypes': [
                'AWS::EC2::Instance',
                'AWS::RDS::DBInstance',
                'AWS::S3::Bucket',
                'AWS::Lambda::Function',
                'AWS::DynamoDB::Table'
            ]
        }
    }

    config_client.put_config_rule(ConfigRule=config_rule)

    return {
        'rule_name': 'required-tags-compliance',
        'enforcement': 'Detect non-compliant resources',
        'remediation': 'Manual tagging or automated via SSM Automation'
    }

Cost Allocation Reporting

Generate monthly cost reports grouped by tags for chargeback/showback:

def generate_cost_allocation_report(
    month: str,  # Format: 'YYYY-MM'
    group_by_tags: List[str] = ['Project', 'Environment', 'CostCenter']
) -> Dict:
    """
    Generate cost allocation report grouped by tags
    Identifies untagged costs requiring attention
    """
    ce_client = boto3.client('ce')

    from datetime import datetime, timedelta
    start_date = datetime.strptime(f'{month}-01', '%Y-%m-%d')

    # Calculate end date (last day of month)
    if start_date.month == 12:
        end_date = start_date.replace(year=start_date.year + 1, month=1, day=1)
    else:
        end_date = start_date.replace(month=start_date.month + 1, day=1)

    end_date = end_date - timedelta(days=1)

    # Get cost and usage by tags
    cost_by_tags = {}

    for tag_key in group_by_tags:
        response = ce_client.get_cost_and_usage(
            TimePeriod={
                'Start': start_date.strftime('%Y-%m-%d'),
                'End': end_date.strftime('%Y-%m-%d')
            },
            Granularity='MONTHLY',
            Metrics=['UnblendedCost'],
            GroupBy=[
                {'Type': 'TAG', 'Key': tag_key}
            ]
        )

        # Process results
        tag_costs = {}
        for result in response['ResultsByTime']:
            for group in result['Groups']:
                tag_value = group['Keys'][0].split('$')[1] if '$' in group['Keys'][0] else 'Untagged'
                cost = float(group['Metrics']['UnblendedCost']['Amount'])
                tag_costs[tag_value] = cost

        cost_by_tags[tag_key] = tag_costs

    # Calculate total and untagged percentages
    total_response = ce_client.get_cost_and_usage(
        TimePeriod={
            'Start': start_date.strftime('%Y-%m-%d'),
            'End': end_date.strftime('%Y-%m-%d')
        },
        Granularity='MONTHLY',
        Metrics=['UnblendedCost']
    )

    total_cost = float(total_response['ResultsByTime'][0]['Total']['UnblendedCost']['Amount'])
    tagged_cost = sum(cost_by_tags.get('Project', {}).values())
    untagged_cost = total_cost - tagged_cost
    untagged_percentage = (untagged_cost / total_cost) * 100 if total_cost > 0 else 0

    return {
        'month': month,
        'total_cost': round(total_cost, 2),
        'tagged_cost': round(tagged_cost, 2),
        'untagged_cost': round(untagged_cost, 2),
        'untagged_percentage': round(untagged_percentage, 1),
        'breakdown_by_tags': cost_by_tags,
        'alert': 'High untagged cost - improve compliance' if untagged_percentage > 20 else None
    }

Tagging rules that hold up:

  • Required tags: Environment (production/staging/development), Project, CostCenter, Owner
  • Activate cost allocation tags in the Billing Console before using them, since nothing is tracked retroactively
  • Pin the allowed tag values; production vs prod vs Production silently splits one cost center into three
  • Use AWS Config to detect untagged resources, and enforce at creation time rather than remediating by hand
  • Target: <10% untagged resource costs

Optimization Techniques Comparison

TechniqueSavingsImplementation EffortRiskBest For
Right-sizing (Compute Optimizer)20-40%LowLowOver-provisioned instances
Savings PlansUp to 66%LowMediumPredictable baseline
Reserved InstancesUp to 72%LowMediumStable workloads
Spot Instances70-90%MediumMediumFault-tolerant workloads
Lambda memory optimization20-50%LowLowLambda-heavy architectures
S3 Intelligent-Tiering40-95%LowLowLarge storage, mixed access
Instance scheduling70%MediumLowNon-production environments

Where the Default Breaks

Visibility first, then right-sizing, then commitments holds for most production accounts, and the reason is sequencing risk. A Savings Plan bought against unmeasured, over-provisioned usage locks in the wrong baseline for one to three years. A right-sizing pass done first shrinks the commitment you need to buy in the first place.

Three situations justify breaking that order. A workload whose shape is fixed by something outside your control, such as a licensed database pinned to one instance family, can take a Reserved Instance immediately, because right-sizing will not move it. An account under an immediate spend ceiling can buy a No Upfront commitment first and right-size afterwards, since the discount applies from the hour it activates and No Upfront asks for no capital. A batch tier that already tolerates interruption can move to Spot before anything else is measured, because a 70-90% discount dwarfs whatever right-sizing would have found there.

Whichever entry point you take, activate cost allocation tags before the first purchase. Tags are not retroactive, so every month without them is a month of spend nobody will be able to attribute later.

References

Related posts