6 min read
Core Compute & Storage
Right-sizing Lambda and EC2 against measured usage, S3 lifecycle policies that actually control cost, and never trusting ephemeral disk to persist.
Principles
Lambda cold starts are a design constraint, not a bug to eliminate
A function outside a VPC with a small deployment package starts fast; one that pulls in a large dependency tree or sits in a VPC pays a real cold-start cost. Design the timeout and user experience around this — don't pretend it doesn't exist.
Right-size compute against measured usage, not a guess
An EC2 instance or Lambda memory allocation chosen once at launch and never revisited is either wasted spend or a silent bottleneck. CloudWatch metrics exist specifically to answer this.
S3 lifecycle policies are the difference between cheap storage and an ever-growing bill
Objects that don't get moved to Infrequent Access or Glacier, or expired, accumulate storage cost indefinitely for data nobody's read in years.
Ephemeral compute shouldn't hold state it can't afford to lose
A Lambda's `/tmp` or an ECS task's local disk can vanish between invocations or on a task restart. Anything that must survive belongs in S3, RDS, or DynamoDB, not local disk.
Autoscaling needs both a floor and a ceiling stated deliberately
A minimum that's too low means a cold cluster taking the first burst of traffic badly. A maximum that's unset, or too high, means a runaway process or attack can scale cost as fast as it scales instances.
Patterns
What goes wrong, what to do instead, and why the difference matters.
Lambda memory left at the default
Avoid
resource "aws_lambda_function" "process_image" {
memory_size = 128 # default, never revisited
timeout = 30
}
# CPU is allocated proportionally to memory in Lambda — 128MB
# gives this CPU-bound image processing job a fraction of a
# vCPU, so it runs slowly and sometimes times out.The default memory allocation wasn't chosen for this workload's actual CPU needs, and Lambda's proportional CPU scaling means it's quietly starved.
Prefer
resource "aws_lambda_function" "process_image" {
memory_size = 1024 # sized against actual CloudWatch duration/memory metrics
timeout = 30
}Memory (and the CPU that scales with it) is sized against measured behaviour, not left at whatever the default happened to be.
No S3 lifecycle policy on an append-only bucket
Avoid
resource "aws_s3_bucket" "logs" {
bucket = "app-access-logs"
}
# No lifecycle rule — every log object ever written stays in
# Standard storage class forever.Storage cost grows linearly and permanently with log volume, even for logs from three years ago that will never be read again.
Prefer
resource "aws_s3_bucket_lifecycle_configuration" "logs" {
bucket = aws_s3_bucket.logs.id
rule {
id = "age-out-logs"
status = "Enabled"
transition { days = 30 storage_class = "STANDARD_IA" }
transition { days = 90 storage_class = "GLACIER" }
expiration { days = 365 }
}
}Objects age into cheaper storage automatically and eventually expire, so cost tracks actual retention needs instead of growing forever.
State written to ephemeral local disk
Avoid
// handler.ts — writes a processing checkpoint to /tmp, assuming
// it'll be there on the next invocation.
import { writeFileSync } from "node:fs";
export const handler = async (state: unknown) => {
writeFileSync("/tmp/checkpoint.json", JSON.stringify(state));
// The next invocation may hit a cold, different execution
// environment where /tmp is empty.
};`/tmp` is scoped to one execution environment and isn't guaranteed to persist or be shared across invocations.
Prefer
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
export const handler = async (state: unknown, jobId: string) => {
const s3 = new S3Client({});
await s3.send(new PutObjectCommand({
Bucket: "app-checkpoints",
Key: `${jobId}.json`,
Body: JSON.stringify(state),
}));
};The checkpoint survives regardless of which execution environment handles the next invocation, because it lives in genuinely durable storage.
Review checklist
What I look for when reviewing a pull request that touches this area.
- Lambda memory/CPU and EC2 instance sizing are set against measured CloudWatch metrics, not left at defaults or initial guesses.
- Every S3 bucket that accumulates objects over time has an explicit lifecycle policy (transition and/or expiration).
- Nothing that must survive between invocations or restarts is written only to ephemeral local disk (`/tmp`, container filesystem).
- Autoscaling groups and Lambda concurrency have both a deliberate floor and an explicit ceiling.
- Cold-start-sensitive Lambda functions avoid unnecessary VPC attachment and heavy dependency trees.
- Compute and storage costs are reviewed periodically against actual usage, not set once at launch and forgotten.
Take the skill
Everything above, generated into a skill file your agent can load. Copy it, or download it and commit it to your repo.
.claude/skills/aws-compute-and-storage/SKILL.mdDrop into .claude/skills/ or .cursor/skills/ — the frontmatter drives when the skill loads.
---
name: aws-compute-and-storage
description: >-
AWS compute and storage fundamentals: right-sizing Lambda and EC2 against
measured usage, S3 lifecycle policies, avoiding ephemeral-disk state loss,
and deliberate autoscaling bounds. Use when provisioning or reviewing
compute and storage resources.
argument-hint: "[resource]"
license: MIT
metadata:
author: John Felix Lim
version: "1.0.0"
---
# Core Compute & Storage
Apply when provisioning compute or storage, or reviewing cost or reliability. Check whether sizing decisions are backed by actual metrics before assuming a default or an initial guess is still correct.
## Rules
1. **Lambda cold starts are a design constraint, not a bug to eliminate** — A function outside a VPC with a small deployment package starts fast; one that pulls in a large dependency tree or sits in a VPC pays a real cold-start cost. Design the timeout and user experience around this — don't pretend it doesn't exist.
2. **Right-size compute against measured usage, not a guess** — An EC2 instance or Lambda memory allocation chosen once at launch and never revisited is either wasted spend or a silent bottleneck. CloudWatch metrics exist specifically to answer this.
3. **S3 lifecycle policies are the difference between cheap storage and an ever-growing bill** — Objects that don't get moved to Infrequent Access or Glacier, or expired, accumulate storage cost indefinitely for data nobody's read in years.
4. **Ephemeral compute shouldn't hold state it can't afford to lose** — A Lambda's `/tmp` or an ECS task's local disk can vanish between invocations or on a task restart. Anything that must survive belongs in S3, RDS, or DynamoDB, not local disk.
5. **Autoscaling needs both a floor and a ceiling stated deliberately** — A minimum that's too low means a cold cluster taking the first burst of traffic badly. A maximum that's unset, or too high, means a runaway process or attack can scale cost as fast as it scales instances.
## Patterns
### Lambda memory left at the default
**Avoid** — The default memory allocation wasn't chosen for this workload's actual CPU needs, and Lambda's proportional CPU scaling means it's quietly starved.
```hcl
resource "aws_lambda_function" "process_image" {
memory_size = 128 # default, never revisited
timeout = 30
}
# CPU is allocated proportionally to memory in Lambda — 128MB
# gives this CPU-bound image processing job a fraction of a
# vCPU, so it runs slowly and sometimes times out.
```
**Prefer** — Memory (and the CPU that scales with it) is sized against measured behaviour, not left at whatever the default happened to be.
```hcl
resource "aws_lambda_function" "process_image" {
memory_size = 1024 # sized against actual CloudWatch duration/memory metrics
timeout = 30
}
```
### No S3 lifecycle policy on an append-only bucket
**Avoid** — Storage cost grows linearly and permanently with log volume, even for logs from three years ago that will never be read again.
```hcl
resource "aws_s3_bucket" "logs" {
bucket = "app-access-logs"
}
# No lifecycle rule — every log object ever written stays in
# Standard storage class forever.
```
**Prefer** — Objects age into cheaper storage automatically and eventually expire, so cost tracks actual retention needs instead of growing forever.
```hcl
resource "aws_s3_bucket_lifecycle_configuration" "logs" {
bucket = aws_s3_bucket.logs.id
rule {
id = "age-out-logs"
status = "Enabled"
transition { days = 30 storage_class = "STANDARD_IA" }
transition { days = 90 storage_class = "GLACIER" }
expiration { days = 365 }
}
}
```
### State written to ephemeral local disk
**Avoid** — `/tmp` is scoped to one execution environment and isn't guaranteed to persist or be shared across invocations.
```ts
// handler.ts — writes a processing checkpoint to /tmp, assuming
// it'll be there on the next invocation.
import { writeFileSync } from "node:fs";
export const handler = async (state: unknown) => {
writeFileSync("/tmp/checkpoint.json", JSON.stringify(state));
// The next invocation may hit a cold, different execution
// environment where /tmp is empty.
};
```
**Prefer** — The checkpoint survives regardless of which execution environment handles the next invocation, because it lives in genuinely durable storage.
```ts
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
export const handler = async (state: unknown, jobId: string) => {
const s3 = new S3Client({});
await s3.send(new PutObjectCommand({
Bucket: "app-checkpoints",
Key: `${jobId}.json`,
Body: JSON.stringify(state),
}));
};
```
## Review checklist
- [ ] Lambda memory/CPU and EC2 instance sizing are set against measured CloudWatch metrics, not left at defaults or initial guesses.
- [ ] Every S3 bucket that accumulates objects over time has an explicit lifecycle policy (transition and/or expiration).
- [ ] Nothing that must survive between invocations or restarts is written only to ephemeral local disk (`/tmp`, container filesystem).
- [ ] Autoscaling groups and Lambda concurrency have both a deliberate floor and an explicit ceiling.
- [ ] Cold-start-sensitive Lambda functions avoid unnecessary VPC attachment and heavy dependency trees.
- [ ] Compute and storage costs are reviewed periodically against actual usage, not set once at launch and forgotten.
---
From the John Felix Lim Frontend Engineering Playbook — https://github.com/JohnFelixLim