Before — EC2 ASG
Client
5ms
CloudFront (existing)
3ms
Application LB
3ms
EC2 ASG (t3.medium ×2)
5ms handler
Express Handler
18ms query
Backend Database
4ms
Serialize
Always-on cost
Manual patching
AMI management
Scaling lag
Warm Latency
After — Serverless
Client
8ms
CloudFront (existing)
12ms
API Gateway HTTP
2ms warm
Lambda (ARM64)
5ms
DynamoDB + Backend DB
2ms
Serialize
Zero idle cost
AWS-managed
Pay per invocation
Auto-scales to zero
Single domain · no CORS
Latency
EC2 Baseline
$0.00
Always-on billing at $0.0416/hr
Serverless
$0.00
Idle · no cost at rest · per-invocation only
💡 Why ~30% cheaper
Before (EC2) — 100% baseline
EC2 compute 58%
ALB 27%
Data Transfer 5%
Operations 10%
After (Serverless) — ~70% of baseline
Lambda 27%
API Gateway 20%
CloudFront + S3 6%
DynamoDB 2%
Operations 10%
−30%
No always-on compute. The EC2 version paid for baseline capacity 24/7, while the serverless backend scaled with actual request volume.
Before — EC2
1
CloudFront + load balancer routing
+8ms total
2
App instance receives request
+3ms
3
Express request handler
+5ms
4
Query backend systems / database
+18ms
5
Build and return response
+4ms
Total: 38ms
After — Serverless
1
CloudFront → API Gateway routing
+20ms
2
Lambda handler starts
warm: +2ms
3
Load lightweight state / mapping
+2ms
4
Query backend systems
+5ms
5
Build and return response
+2ms
Total: ~31ms
Cold Start Trade-off: The occasional ~285ms Lambda cold start is an accepted trade-off for an internal API because (1) the cost savings far outweigh latency impact, (2) requests naturally warm the function, and (3) provisioned concurrency could mitigate if needed in future.
Monthly infra cost
100%
EC2 baseline
~70%
of EC2 cost
↓ ~30% cost reduction
EC2
SL
Lambda scales with actual request volume instead of paying for idle EC2 capacity 24/7.
Daily API requests
100K/day
EC2 ASG · reserved
100K/day
Lambda · on-demand
Same volume · scales to zero at idle
Same request load. EC2 holds reserved capacity even at 0 req/s. Lambda provisions only when called — no idle billing.
Operational overhead
High
AMI · patching · ASG
Minimal
AWS-managed runtime
DevOps hours freed
No AMI builds, no OS patching, no ASG tuning. AWS manages the runtime. Zero instances to monitor.
Cold start latency
N/A
always warm
~300ms
occasional
Accepted tradeoff — internal API
Cold starts were an acceptable trade-off for an internal, non-latency-critical API.
Architecture decisions
Topic
Before — EC2
After — Serverless
Compute
EC2 ASG · t3.medium ×2
Lambda ARM64 Graviton2 · Node.js
Pay-per-invocation
Routing
Application Load Balancer · separate domain from SPA
CloudFront (existing) → API Gateway HTTP API
Single domain · no CORS · weighted origin cutover
App state
In-memory per instance (not shared)
DynamoDB on-demand
Stateless Lambda · shared state
Deployment
GitHub Actions → AMI bake → ASG refresh
GitHub Actions → package → Lambda · S3 sync + CF invalidation
No AMI · faster deploys · single pipeline
Provisioned concurrency
N/A · always warm
Evaluated · not adopted
Cold starts acceptable — internal API · not latency-critical
Patching / AMI
Regular AMI rebuilds · SSM patching
Eliminated — AWS-managed runtime
Ops overhead removed
IaC
Terraform (ASG · ALB · IAM)
Terraform (API GW · Lambda · DynamoDB · IAM)
Fully automated · Git-tracked
Platform Engineer
Me — infrastructure ownership
🏗️
Full serverless infrastructure in Terraform
API Gateway HTTP API, Lambda function config (ARM64), DynamoDB table, all IAM roles and supporting resources. Existing CloudFront distribution and S3 bucket managed as IaC alongside the new backend.
TerraformAPI GatewayLambdaDynamoDBCloudFront
🚀
GitHub Actions CI/CD pipeline
Terraform plan/apply stages, docker buildx for ARM64 cross-compilation, Lambda zip packaging and deploy, S3 sync for frontend assets, CloudFront cache invalidation — single pipeline for the full stack.
GitHub Actionsdocker buildxARM64CF invalidation
🔀
CloudFront routing & zero-downtime cutover
Reconfigured existing CloudFront distribution: /api/* forwards to API Gateway, /* serves the SPA from S3 — single domain, no CORS changes needed. Used weighted origins to shift traffic from ALB to API Gateway over 48 hours with no downtime.
CloudFront behavioursWeighted originsOACZero-downtime
📐
Architecture decisions & tradeoff evaluation
Lambda vs Fargate, HTTP API vs REST API, DynamoDB vs Aurora Serverless. Evaluated provisioned concurrency — accepted cold starts as internal API with adequate traffic to keep functions warm.
ADRCost analysisDynamoDB design
🔐
Security & least-privilege IAM
Lambda execution roles scoped to exact DynamoDB actions per function. API Gateway resource policies, VPC Lambda integration for backend DB access.
IAMResource policiesVPC
Backend Developer
Collaborator — application ownership
✂️
Express → Lambda handler refactoring
Decomposed monolithic Express app into discrete per-route Lambda handlers — adapting middleware chains, connection management, and request/response lifecycle to Lambda's event-driven model.
Node.jsLambda handler pattern
💾
DynamoDB data access layer
Implemented reads/writes for lightweight app state (user context, cached metadata, workflow mappings). Core aggregation data stayed in existing backend — no data migration required.
DynamoDB SDKData modellingGSI
🔌
Frontend API client integration
Removed direct ALB endpoint references — frontend already called the CloudFront domain, so no domain changes were needed. Adapted API calls to the new route contract defined in infra (/api/* paths).
API integrationRoute contractALB removal
📊
Observability & testing
CloudWatch Logs structured logging, X-Ray tracing for end-to-end request visibility. Unit tests for handler logic, integration tests against DynamoDB Local.
CloudWatchX-RayDynamoDB Local
🤝
Where infra and app met — the integration layer
Platform engineering owned the infrastructure contract; the developer owned the application code. Close collaboration was required at every boundary.
CloudFront behaviour paths (/api/*) had to match exactly what the frontend called — defined together to avoid CORS edge cases and cache collisions
API Gateway route names and Lambda function identifiers had to align with the handler refactoring in progress
DynamoDB schema was co-designed: platform set capacity and access patterns, developer drove key structure based on query needs
Lambda IAM roles (written in Terraform) were scoped per-function — required understanding each handler's exact access to the existing backend
Zero-downtime cutover via CloudFront weighted origins — gradual traffic shift from ALB to API Gateway over 48 hours