Header menu logo FsCDK

AWS Cost Optimization Strategies

AWS Cost Optimization

Price estimates based on November 2025. Practical approaches to reduce infrastructure costs while maintaining security and functionality.

Environment-Specific Cost Tiers

Different environments have different cost/security trade-offs:

#r "../src/bin/Release/net8.0/publish/Amazon.JSII.Runtime.dll"
#r "../src/bin/Release/net8.0/publish/Constructs.dll"
#r "../src/bin/Release/net8.0/publish/Amazon.CDK.Lib.dll"
#r "../src/bin/Release/net8.0/publish/FsCDK.dll"

open FsCDK
open Amazon.CDK
open Amazon.CDK.AWS.EC2
open Amazon.CDK.AWS.RDS

Cost-Optimized Development Environment

Minimize costs in development without sacrificing functionality:

stack "DevEnvironment" {
    // Minimal VPC - single NAT or no NAT
    let! devVpc =
        vpc "DevVPC" {
            maxAzs 2
            natGateways 0 // Save ~$32/month - use public subnets
            cidr "10.0.0.0/16"
        }

    // Smallest viable database
    rdsInstance "DevDatabase" {
        vpc devVpc
        postgresEngine PostgresEngineVersion.VER_15
        instanceType (InstanceType.Of(InstanceClass.BURSTABLE3, InstanceSize.MICRO))
        databaseName "devdb"

        // Cost optimizations
        multiAz false // Save ~50% on RDS
        backupRetentionDays 1.0 // Minimal backups
        deletionProtection false // Easy to tear down

        // Security maintained
        storageEncrypted true
        publiclyAccessible false

        removalPolicy RemovalPolicy.DESTROY
    }
}

Dev Environment Cost: ~$20-40/month

Service

Cost Saving

NAT Gateway removal

-$32/month

Single-AZ RDS

-$25/month

No VPC Endpoints

-$7-15/month

Smaller instances

-$20-50/month

Production Environment with Security

Full security and high availability:

stack "ProductionEnvironment" {
    // Production VPC with HA
    let! prodVpc =
        vpc "ProdVPC" {
            maxAzs 3
            natGateways 2 // HA: one per AZ
            cidr "10.0.0.0/16"
        }

    // Production-grade database
    rdsInstance "ProdDatabase" {
        vpc prodVpc
        postgresEngine PostgresEngineVersion.VER_15
        instanceType (InstanceType.Of(InstanceClass.MEMORY5, InstanceSize.LARGE))
        databaseName "proddb"

        // High availability
        multiAz true
        backupRetentionDays 30.0

        // Production security
        deletionProtection true
        storageEncrypted true
        enablePerformanceInsights true

        removalPolicy RemovalPolicy.RETAIN
    }

    // VPC Endpoints for private AWS service access
    gatewayVpcEndpoint "S3Endpoint" {
        vpc prodVpc
        service GatewayVpcEndpointAwsService.S3
    }

    gatewayVpcEndpoint "DynamoDBEndpoint" {
        vpc prodVpc
        service GatewayVpcEndpointAwsService.DYNAMODB
    }
}

Production Environment Cost: ~$150-300/month

NAT Gateway Cost Analysis

NAT Gateways are often the largest single cost in VPC architecture:

Configuration

Monthly Cost

Use Case

No NAT

$0

Dev with public subnets

1 NAT Gateway

~$32 + data

Dev/staging

2 NAT Gateways

~$64 + data

Production (HA)

3+ NAT Gateways

~$96+

Enterprise (multi-AZ HA)

Alternatives to NAT Gateways

  1. Public Subnets - Lambda/ECS in public subnets (dev only)
  2. VPC Endpoints - Private AWS service access without NAT
  3. AWS PrivateLink - Direct service connections
  4. Proxy Instances - Single t4g.nano as NAT (~$3/month)

VPC Endpoint Cost Trade-offs

Interface VPC Endpoints cost ~$7-15/month each but save NAT data transfer costs:

Scenario

NAT Cost

VPC Endpoint

Recommendation

Low traffic

\(32 +\)5/mo data

$7/mo fixed

Use NAT

Medium traffic

\(32 +\)20/mo data

$7/mo fixed

Use VPC Endpoint

High traffic

\(32 +\)50+/mo data

$7/mo fixed

Use VPC Endpoint

Gateway endpoints (S3, DynamoDB) are free - always use them!

Database Cost Optimization

Instance Class Selection

Workload

Dev

Staging

Production

Low traffic

t3.micro (\(13) | t3.small (\)25)

t3.medium ($50)

Medium traffic

t3.small (\(25) | t3.medium (\)50)

m5.large ($140)

High traffic

t3.medium (\(50) | m5.large (\)140)

r5.xlarge ($370)

Multi-AZ Cost Impact

Storage Optimization

// Define VPC first

stack "DatabaseStack" {

    let! myVpc =
        vpc "MyVpc" {
            maxAzs 2
            natGateways 1
            cidr "10.0.0.0/16"
        }

    rdsInstance "OptimizedDB" {
        vpc myVpc
        postgresEngine

        // Auto-scaling storage - pay only for what you use
        allocatedStorage 20 // Start small (20GB minimum)
        maxAllocatedStorage 100 // Auto-scale up to 100GB as needed

        storageType StorageType.GP3 // 20% cheaper than gp2, better performance
    }

}

Lambda Cost Optimization

Lambda with FsCDK production defaults is already optimized:

Lambda Cost Formula

Cost = (Duration × Memory × Price) + Requests

Load Balancer Cost Analysis

Load Balancer

Monthly Cost

Use Case

Application LB

~$16 + data

HTTP/HTTPS traffic

Network LB

~$16 + data

TCP/UDP traffic

Classic LB

~$18 + data

Legacy (avoid)

Anti-pattern: Using both ALB + NLB together (~$32/month) Better: Use ALB only for HTTP/HTTPS APIs

CloudWatch Costs

Reduce monitoring costs without losing visibility:

Log Retention Strategy

Environment

Retention

Cost Impact

Dev

3-7 days

Minimal

Staging

14 days

Low

Production

30-90 days

Moderate

Custom Metrics

// ❌ BAD: Too many unique metric combinations
CloudWatch.putMetric("RequestCount", dimensions = [
    ("UserId", userId),        // High cardinality!
    ("Endpoint", endpoint),
    ("Method", method)
])

// ✅ GOOD: Aggregate high-cardinality dimensions
CloudWatch.putMetric("RequestCount", dimensions = [
    ("Endpoint", endpoint),    // Low cardinality
    ("Method", method)
])

S3 Cost Optimization

Storage Classes

Class

Cost/GB

Retrieval

Use Case

Standard

$0.023

Free

Active data

IA

\(0.0125 |\)0.01/GB

30 days old

Glacier

$0.004

Hours

90 days old

Lifecycle Policies

bucket "CostOptimizedStorage" {
    versioned true

    yield
        lifecycleRule {
            enabled true

            transitions
                [ transition {
                      storageClass Amazon.CDK.AWS.S3.StorageClass.INFREQUENT_ACCESS
                      transitionAfter (Duration.Days 30.0)
                  }
                  transition {
                      storageClass Amazon.CDK.AWS.S3.StorageClass.GLACIER
                      transitionAfter (Duration.Days 90.0)
                  } ]

            // Delete old versions
            noncurrentVersionExpiration (Duration.Days 90.0)
        }
}

Environment Cost Comparison

Component

Dev

Staging

Production

VPC (NAT)

\(0 |\)32

$64

RDS

\(13 |\)50

$100+

Lambda

\(5 |\)20

$50+

ALB

\(16 |\)16

$16

S3

\(2 |\)5

$20+

CloudWatch

\(2 |\)5

$20+

Total

*~$40* | ~$130

*~$270+*

Cost Monitoring and Alerts

Set up budget alerts for each environment:

// Use AWS Budgets to alert on cost thresholds
// Dev: Alert at $50
// Staging: Alert at $150
// Production: Alert at $300

Quick Wins Checklist

Common Cost Anti-Patterns

❌ Don't Do This

  1. Multi-AZ in dev - Wastes 50% on RDS
  2. NAT Gateways everywhere - $32/month per gateway
  3. Interface VPC Endpoints without traffic analysis - $7-15/month each
  4. Oversized RDS instances - Start small, scale up
  5. Infinite log retention - Use 7-30 days max
  6. No resource tagging - Can't track costs per project
  7. Unused load balancers - $16/month even with no traffic

✅ Do This Instead

  1. Single-AZ in dev/staging
  2. Public subnets in dev (no NAT)
  3. Gateway endpoints (free) first, interface endpoints only if high traffic
  4. Right-size instances based on CloudWatch metrics
  5. 7-day retention in dev, 30 days in prod
  6. Tag everything: Environment, Project, Owner
  7. Delete unused resources in automated cleanup

Monthly Cost Targets

Set realistic cost targets per environment:

Resources

namespace FsCDK
namespace Amazon
namespace Amazon.CDK
namespace Amazon.CDK.AWS
namespace Amazon.CDK.AWS.EC2
namespace Amazon.CDK.AWS.RDS
val stack: name: string -> StackBuilder
<summary>Creates an AWS CDK Stack construct.</summary>
<param name="name">The name of the stack.</param>
<code lang="fsharp"> stack "MyStack" { lambda myFunction bucket myBucket } </code>
val devVpc: IVpc
val vpc: name: string -> VpcBuilder
<summary>Creates a VPC configuration with AWS best practices.</summary>
<param name="name">The VPC name.</param>
<code lang="fsharp"> vpc "MyVpc" { maxAzs 2 natGateways 1 cidr "10.0.0.0/16" } </code>
custom operation: maxAzs (int) Calls VpcBuilder.MaxAzs
<summary>Sets the maximum number of Availability Zones to use.</summary>
<param name="config">The current VPC configuration.</param>
<param name="maxAzs">The maximum number of AZs (default: 2 for HA).</param>
<code lang="fsharp"> vpc "MyVpc" { maxAzs 3 } </code>
custom operation: natGateways (int) Calls VpcBuilder.NatGateways
<summary>Sets the number of NAT Gateways.</summary>
<param name="config">The current VPC configuration.</param>
<param name="natGateways">The number of NAT gateways (default: 1 for cost optimization).</param>
<code lang="fsharp"> vpc "MyVpc" { natGateways 2 } </code>
custom operation: cidr (string) Calls VpcBuilder.Cidr
<summary>Sets the CIDR block for the VPC.</summary>
<param name="config">The current VPC configuration.</param>
<param name="cidr">The CIDR block (e.g., "10.0.0.0/16").</param>
<code lang="fsharp"> vpc "MyVpc" { cidr "10.0.0.0/16" } </code>
val rdsInstance: name: string -> DatabaseInstanceBuilder
<summary>Creates an RDS Database Instance with AWS best practices.</summary>
<param name="name">The database instance name.</param>
<code lang="fsharp"> rdsInstance "MyDatabase" { vpc myVpc postgresEngine PostgresEngineVersion.VER_15 instanceType (InstanceType.Of(InstanceClass.BURSTABLE3, InstanceSize.SMALL)) multiAz true backupRetentionDays 7.0 } </code>
custom operation: vpc (IVpc) Calls DatabaseInstanceBuilder.Vpc
<summary>Sets the VPC.</summary>
custom operation: postgresEngine (PostgresEngineVersion option) Calls DatabaseInstanceBuilder.PostgresEngine
<summary>Sets PostgreSQL as the database engine with a specific version.</summary>
type PostgresEngineVersion = inherit DeputyBase static member Of: postgresFullVersion: string * postgresMajorVersion: string * ?postgresFeatures: IPostgresEngineFeatures -> PostgresEngineVersion member PostgresFullVersion: string member PostgresMajorVersion: string static member VER_10: PostgresEngineVersion static member VER_10_1: PostgresEngineVersion static member VER_10_10: PostgresEngineVersion static member VER_10_11: PostgresEngineVersion static member VER_10_12: PostgresEngineVersion static member VER_10_13: PostgresEngineVersion ...
property PostgresEngineVersion.VER_15: PostgresEngineVersion with get
custom operation: instanceType (InstanceType) Calls DatabaseInstanceBuilder.InstanceType
<summary>Sets the instance type.</summary>
[<Struct>] type InstanceType = | PROVISIONED = 0 | SERVERLESS_V2 = 1
InstanceType.Of(instanceClass: InstanceClass, instanceSize: InstanceSize) : InstanceType
[<Struct>] type InstanceClass = | STANDARD3 = 0 | M3 = 1 | STANDARD4 = 2 | M4 = 3 | STANDARD5 = 4 | M5 = 5 | STANDARD5_NVME_DRIVE = 6 | M5D = 7 | STANDARD5_AMD = 8 | M5A = 9 ...
field InstanceClass.BURSTABLE3: InstanceClass = 172
[<Struct>] type InstanceSize = | NANO = 0 | MICRO = 1 | SMALL = 2 | MEDIUM = 3 | LARGE = 4 | XLARGE = 5 | XLARGE2 = 6 | XLARGE3 = 7 | XLARGE4 = 8 | XLARGE6 = 9 ...
field InstanceSize.MICRO: InstanceSize = 1
custom operation: databaseName (string) Calls DatabaseInstanceBuilder.DatabaseName
<summary>Sets the database name.</summary>
custom operation: multiAz (bool) Calls DatabaseInstanceBuilder.MultiAz
<summary>Enables or disables Multi-AZ deployment.</summary>
custom operation: backupRetentionDays (float) Calls DatabaseInstanceBuilder.BackupRetentionDays
<summary>Sets the backup retention period in days.</summary>
custom operation: deletionProtection (bool) Calls DatabaseInstanceBuilder.DeletionProtection
<summary>Enables or disables deletion protection.</summary>
custom operation: storageEncrypted (bool) Calls DatabaseInstanceBuilder.StorageEncrypted
<summary>Enables storage encryption.</summary>
custom operation: publiclyAccessible (bool) Calls DatabaseInstanceBuilder.PubliclyAccessible
<summary>Sets whether the database is publicly accessible.</summary>
custom operation: removalPolicy (RemovalPolicy) Calls DatabaseInstanceBuilder.RemovalPolicy
<summary>Sets the removal policy.</summary>
[<Struct>] type RemovalPolicy = | DESTROY = 0 | RETAIN = 1 | SNAPSHOT = 2 | RETAIN_ON_UPDATE_OR_DELETE = 3
field RemovalPolicy.DESTROY: RemovalPolicy = 0
val prodVpc: IVpc
field InstanceClass.MEMORY5: InstanceClass = 22
field InstanceSize.LARGE: InstanceSize = 4
custom operation: enablePerformanceInsights (bool) Calls DatabaseInstanceBuilder.EnablePerformanceInsights
<summary>Enables performance insights.</summary>
field RemovalPolicy.RETAIN: RemovalPolicy = 1
val gatewayVpcEndpoint: name: string -> GatewayVpcEndpointBuilder
<summary>Creates a Gateway VPC Endpoint (for S3, DynamoDB).</summary>
<param name="name">The endpoint name.</param>
<code lang="fsharp"> gatewayVpcEndpoint "S3Endpoint" { vpc myVpc service GatewayVpcEndpointAwsService.S3 } </code>
custom operation: vpc (IVpc) Calls GatewayVpcEndpointBuilder.Vpc
<summary>Sets the VPC for the endpoint.</summary>
<param name="config">The current Gateway VPC Endpoint configuration.</param>
<param name="vpc">The VPC.</param>
<code lang="fsharp"> gatewayVpcEndpoint "S3Endpoint" { vpc myVpc } </code>
custom operation: service (IGatewayVpcEndpointService) Calls GatewayVpcEndpointBuilder.Service
<summary>Sets the service for the endpoint (e.g., S3, DynamoDB).</summary>
<param name="config">The current Gateway VPC Endpoint configuration.</param>
<param name="service">The gateway endpoint service (e.g., GatewayVpcEndpointAwsService.S3).</param>
<code lang="fsharp"> gatewayVpcEndpoint "S3Endpoint" { service GatewayVpcEndpointAwsService.S3 } </code>
Multiple items
type GatewayVpcEndpointAwsService = inherit DeputyBase interface IGatewayVpcEndpointService new: name: string * ?prefix: string -> unit member Name: string static member DYNAMODB: GatewayVpcEndpointAwsService static member S3: GatewayVpcEndpointAwsService static member S3_EXPRESS: GatewayVpcEndpointAwsService

--------------------
GatewayVpcEndpointAwsService(name: string, ?prefix: string) : GatewayVpcEndpointAwsService
property GatewayVpcEndpointAwsService.S3: GatewayVpcEndpointAwsService with get
property GatewayVpcEndpointAwsService.DYNAMODB: GatewayVpcEndpointAwsService with get
val myVpc: IVpc
custom operation: allocatedStorage (int) Calls DatabaseInstanceBuilder.AllocatedStorage
<summary>Sets the allocated storage in GB.</summary>
custom operation: maxAllocatedStorage (int) Calls DatabaseInstanceBuilder.MaxAllocatedStorage
<summary>Sets the maximum allocated storage in GB for autoscaling.</summary>
custom operation: storageType (StorageType) Calls DatabaseInstanceBuilder.StorageType
<summary>Sets the storage type.</summary>
[<Struct>] type StorageType = | STANDARD = 0 | GP2 = 1 | GP3 = 2 | IO1 = 3 | IO2 = 4
field StorageType.GP3: StorageType = 2
val bucket: name: string -> BucketBuilder
custom operation: versioned (bool) Calls BucketBuilder.Versioned
<summary> Enables or disables versioning for the S3 bucket. **Security Best Practice:** Enable versioning for: - Critical data that requires audit trails - Data subject to compliance requirements (HIPAA, SOC2, etc.) - Production buckets storing business data **Cost Consideration:** Versioning stores all versions of objects, increasing storage costs. Only disable for: - Temporary/cache buckets - Build artifacts with short lifecycle - Development/testing buckets **Default:** false (opt-in for cost optimization) </summary>
<param name="value">True to enable versioning, false to disable.</param>
<param name="config">The current bucket configuration.</param>
<code lang="fsharp"> bucket "production-data" { versioned true // Enable for production } bucket "cache-bucket" { versioned false // Disable for temp data } </code>
val lifecycleRule: LifecycleRuleBuilder
custom operation: enabled (bool) Calls LifecycleRuleBuilder.Enabled
custom operation: transitions (AWS.S3.ITransition list) Calls LifecycleRuleBuilder.Transitions
val transition: TransitionBuilder
<summary> Creates an S3 lifecycle transition rule for moving objects to different storage classes. Transitions reduce storage costs by automatically moving objects to cheaper storage tiers. </summary>
<code lang="fsharp"> transition { storageClass StorageClass.GLACIER transitionAfter (Duration.Days(90.0)) } </code>
custom operation: storageClass (AWS.S3.StorageClass) Calls TransitionBuilder.StorageClass
<summary> Sets the storage class to transition to. Common classes: GLACIER (low-cost archival), DEEP_ARCHIVE (lowest cost, rare access), INTELLIGENT_TIERING (automatic cost optimization), GLACIER_IR (instant retrieval). </summary>
<param name="storageClass">The target storage class.</param>
namespace Amazon.CDK.AWS.S3
Multiple items
type StorageClass = inherit DeputyBase new: value: string -> unit member ToString: unit -> string member Value: string static member DEEP_ARCHIVE: StorageClass static member GLACIER: StorageClass static member GLACIER_INSTANT_RETRIEVAL: StorageClass static member INFREQUENT_ACCESS: StorageClass static member INTELLIGENT_TIERING: StorageClass static member ONE_ZONE_INFREQUENT_ACCESS: StorageClass

--------------------
AWS.S3.StorageClass(value: string) : AWS.S3.StorageClass
property AWS.S3.StorageClass.INFREQUENT_ACCESS: AWS.S3.StorageClass with get
custom operation: transitionAfter (Duration) Calls TransitionBuilder.TransitionAfter
<summary> Sets when objects transition after creation (use Duration.Days()). Example: transitionAfter (Duration.Days(90.0)) moves objects after 90 days. </summary>
<param name="duration">Time after object creation to transition.</param>
type Duration = inherit DeputyBase member FormatTokenToNumber: unit -> string member IsUnresolved: unit -> bool member Minus: rhs: Duration -> Duration member Plus: rhs: Duration -> Duration member ToDays: ?opts: ITimeConversionOptions -> float member ToHours: ?opts: ITimeConversionOptions -> float member ToHumanString: unit -> string member ToIsoString: unit -> string member ToMilliseconds: ?opts: ITimeConversionOptions -> float ...
Duration.Days(amount: float) : Duration
property AWS.S3.StorageClass.GLACIER: AWS.S3.StorageClass with get
custom operation: noncurrentVersionExpiration (Duration) Calls LifecycleRuleBuilder.NoncurrentVersionExpiration

Type something to start searching.