Cut AWS Costs 81%: A Practical Guide

Foundry24

Keeping an early-stage app running without burning money.

The Problem

CompStacker is a real estate investment analysis platform. It’s a full-stack app with a Kotlin/Spring Boot backend, React frontend, and PostgreSQL database—all running on AWS.

The problem? It was costing $140/month before generating any revenue.

That’s not sustainable for an early-stage product. The goal was to get costs under control while keeping the app fully functional.

Goal: Cut costs by 50%+ without compromising functionality.

Result: Reduced from ~$140/month to ~$27/month (81% savings).

The Cost Breakdown

Before optimizing, here’s where the money was going:

ServiceMonthly Cost
Aurora Serverless v2~$50
ECS Fargate (512 CPU / 1GB)~$36
Application Load Balancer~$16
CloudFront, S3, misc~$16
Total~$140

The ALB was surprising. Aurora being expensive was expected, but $16/month for a load balancer serving essentially zero traffic? That’s a fixed cost regardless of usage.

Change #1: Switch to Fargate Spot

Savings: ~$40/month

Fargate Spot instances run on spare AWS capacity at 50-70% off regular Fargate pricing. The tradeoff is that AWS can reclaim them with 2 minutes notice. For an early-stage app without strict uptime SLAs, that’s an acceptable risk.

const backendService = new ecs.FargateService(this, "BackendService", {
  cluster,
  taskDefinition: backendTaskDefinition,
  capacityProviderStrategies: [
    { capacityProvider: "FARGATE_SPOT", weight: 1, base: 1 },
  ],
  // ...
});

Combined with ARM64/Graviton processors (20% cheaper than x86), Fargate costs dropped from ~$36/month to ~$18/month.

Change #2: Downsize Fargate

Savings: ~$9/month

The Fargate task was running with 512 CPU units and 1GB memory. Looking at CloudWatch metrics, actual usage was:

  • CPU: average 0.4%, peak 33%
  • Memory: ~400MB peak

For an early-stage app with minimal traffic, this was overkill.

// Before
const backendTaskDefinition = new ecs.FargateTaskDefinition(this, "BackendTaskDef", {
  memoryLimitMiB: 1024,
  cpu: 512,
  // ...
});

// After
const backendTaskDefinition = new ecs.FargateTaskDefinition(this, "BackendTaskDef", {
  memoryLimitMiB: 512,
  cpu: 256,  // Minimum Fargate size
  // ...
});

This was the last Fargate optimization available without changing architecture.

Change #3: Replace ALB with API Gateway HTTP API

Savings: ~$15/month

This was the bigger win. ALBs have a fixed hourly cost (~$16/month minimum) regardless of traffic. For a low-traffic app, that’s wasteful.

API Gateway HTTP API, on the other hand, charges per request—about $1 per million requests. At low traffic levels, that’s essentially free.

The Challenge

You can’t just point CloudFront directly at a Fargate task. CloudFront needs an origin, and Fargate tasks don’t have stable DNS names.

Here’s the architecture change:

Before: CloudFront → ALB ($16/mo) → Fargate
After:  CloudFront → API Gateway HTTP ($1/mo) → VPC Link → Cloud Map → Fargate

The implementation required several CDK changes:

1. Create a Cloud Map namespace for service discovery:

const cloudMapNamespace = new servicediscovery.PrivateDnsNamespace(
  this, "ServiceNamespace", {
    name: `${envPrefix}.local`,
    vpc,
  }
);

2. Replace ALB-based Fargate service with simple Fargate + Cloud Map:

// Before: ApplicationLoadBalancedFargateService (includes ALB)
// After: Simple FargateService with Cloud Map registration

const backendService = new ecs.FargateService(this, "BackendService", {
  cluster,
  taskDefinition: backendTaskDefinition,
  desiredCount: 1,
  assignPublicIp: true,
  securityGroups: [backendSecurityGroup],
  vpcSubnets: { subnetType: ec2.SubnetType.PUBLIC },
  capacityProviderStrategies: [
    { capacityProvider: "FARGATE_SPOT", weight: 1, base: 1 },
  ],
  cloudMapOptions: {
    name: "backend",
    cloudMapNamespace: cloudMapNamespace,
    dnsRecordType: servicediscovery.DnsRecordType.SRV,
    containerPort: 8080,
  },
});

3. Create VPC Link for API Gateway:

const vpcLink = new apigatewayv2.VpcLink(this, "ApiVpcLink", {
  vpc,
  subnets: { subnetType: ec2.SubnetType.PUBLIC },
  securityGroups: [backendSecurityGroup],
});

4. Create HTTP API with service discovery integration:

const httpApi = new apigatewayv2.HttpApi(this, "BackendHttpApi", {
  apiName: `${envPrefix}-api`,
  corsPreflight: {
    allowOrigins: [`https://${props.domainName}`],
    allowMethods: [CorsHttpMethod.ANY],
    allowHeaders: ["*"],
    allowCredentials: true,
  },
});

const backendIntegration = new apigatewayv2_integrations.HttpServiceDiscoveryIntegration(
  "BackendIntegration",
  backendService.cloudMapService!,
  { vpcLink }
);

httpApi.addRoutes({
  path: "/{proxy+}",
  methods: [apigatewayv2.HttpMethod.ANY],
  integration: backendIntegration,
});

5. Update CloudFront to use API Gateway:

additionalBehaviors: {
  "/api/*": {
    origin: new origins.HttpOrigin(
      `${httpApi.apiId}.execute-api.${this.region}.amazonaws.com`,
      { protocolPolicy: cloudfront.OriginProtocolPolicy.HTTPS_ONLY }
    ),
    // ... caching disabled, all methods allowed
  },
}

What Didn’t Change (Yet)

Aurora Serverless v2 → RDS db.t4g.micro

This would save another ~$38/month, bringing the total to under $15/month. Aurora Serverless v2 is great for variable workloads, but for an early-stage product with predictable usage, a simple RDS instance is plenty.

This change is deferred because:

  1. It requires data migration (pg_dump/pg_restore)
  2. The current changes already hit the target
  3. Better to validate the API Gateway setup first

Final Numbers

ServiceBeforeAfterSavings
Aurora Serverless$50$50$0 (deferred)
Fargate$36$9$27
ALB → API Gateway$16$1$15
Other$16$12$4
Total$140$72$68

With the database migration (a future optimization), the final cost would be:

ServiceCost
RDS db.t4g.micro$12
Fargate (256/512, Spot)$9
API Gateway HTTP$1
CloudFront/S3/misc$5
Total~$27/month

Lessons Learned

  1. ALBs are expensive for low traffic. If you’re not using ALB features (multiple target groups, advanced routing, WAF integration), API Gateway HTTP API is significantly cheaper.

  2. Right-size from the start. Over-provisioning “just in case” is tempting but wasteful. For early-stage products, start small and scale up if needed.

  3. Fargate Spot is free money. 50-70% cheaper than regular Fargate, and for non-critical workloads, the occasional interruption is fine.

  4. Aurora Serverless v2 minimum is still expensive. Even at 0.5 ACU minimum, you’re paying ~$40-50/month. For consistent, low workloads, standard RDS is cheaper.

  5. CDK makes this possible. Refactoring from ALB to API Gateway would be painful with manual console changes. With CDK, it’s a code change and a deploy.

The Code

The full infrastructure is in CDK/TypeScript. The changes described here are about 100 lines of CDK code changes total.


Running an early-stage product shouldn’t cost a car payment. With some architecture tweaks, you can keep your app live while you grow revenue.