AWS Optimization with SST
This skill covers best practices for optimizing AWS resources using SST, focusing on performance, cost, and developer experience.
Core Principles
-
Right-size resources: Don't over-provision
-
Use SST's defaults: They're already optimized
-
Leverage caching: Reduce redundant work
-
Optimize cold starts: Minimize Lambda initialization time
-
Monitor and iterate: Use data to guide optimization
Lambda Function Optimization
Pattern 1: Function Configuration
// sst.config.ts new sst.aws.Function("Api", { handler: "src/api.handler", memory: "512 MB", // Start here, adjust based on metrics timeout: "30 seconds", // Don't use default 3 seconds architecture: "arm64", // 20% cheaper and often faster nodejs: { esbuild: { minify: true, // Smaller bundle external: [ // Don't bundle AWS SDK v3 "@aws-sdk/*" ] } } });
Memory considerations:
-
Start with 512 MB
-
Monitor execution time vs cost
-
More memory = more CPU = faster execution
-
Sometimes higher memory is cheaper (finishes faster)
Architecture:
-
Use arm64 (Graviton2) for 20% cost savings
-
Same or better performance
-
Works for most workloads
Pattern 2: Bundle Size Optimization
// Optimize imports - tree-shaking friendly ✅ import { DynamoDBClient } from "@aws-sdk/client-dynamodb"; ❌ import * as AWS from "aws-sdk";
// Use specific SDK clients ✅ import { GetCommand } from "@aws-sdk/lib-dynamodb"; ❌ import { DocumentClient } from "aws-sdk/clients/dynamodb";
Bundle size tips:
-
Use AWS SDK v3 (modular)
-
Import only what you need
-
Mark heavy deps as external
-
Use dynamic imports for large libraries
// Dynamic import for rarely-used code export async function generatePDF(data: Data) { const puppeteer = await import("puppeteer"); // Only loads when actually called }
Pattern 3: Connection Reuse
// ✅ Initialize outside handler (reused across invocations) import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
const client = new DynamoDBClient({ maxAttempts: 3, requestHandler: { connectionTimeout: 3000, socketTimeout: 3000 } });
export async function handler(event) { // Use client here }
// ❌ Don't initialize inside handler export async function handler(event) { const client = new DynamoDBClient({}); // Creates new connection every time }
Pattern 4: Provisioned Concurrency
For consistently high traffic:
new sst.aws.Function("HighTraffic", { handler: "src/api.handler", transform: { function: { reservedConcurrentExecutions: 10, // Provisioned concurrency keeps instances warm } } });
When to use:
-
Consistent traffic patterns
-
Latency-sensitive applications
-
Cost justified by reduced cold starts
DynamoDB Optimization
Pattern 1: Table Configuration
const table = new sst.aws.Dynamo("Database", { fields: { pk: "string", sk: "string" }, primaryIndex: { hashKey: "pk", rangeKey: "sk" },
// Use on-demand for variable traffic // Use provisioned for predictable traffic stream: "new-and-old-images", // Only if needed for triggers
transform: { table: { // Enable point-in-time recovery for production pointInTimeRecovery: $app.stage === "production" ? { enabled: true } : undefined,
// TTL for automatic data expiration
timeToLiveAttribute: "expiresAt"
}
} });
Pattern 2: Query Optimization
// ✅ Use Query with specific partition key await client.send(new QueryCommand({ TableName: Resource.Database.name, KeyConditionExpression: "pk = :pk", ExpressionAttributeValues: { ":pk": "USER#123" } }));
// ❌ Don't use Scan unless absolutely necessary await client.send(new ScanCommand({ TableName: Resource.Database.name })); // Scans entire table - expensive and slow!
Pattern 3: Batch Operations
// Write multiple items efficiently import { BatchWriteCommand } from "@aws-sdk/lib-dynamodb";
// Batch up to 25 items per request const batches = chunk(items, 25);
for (const batch of batches) { await client.send(new BatchWriteCommand({ RequestItems: { [Resource.Database.name]: batch.map(item => ({ PutRequest: { Item: item } })) } })); }
Pattern 4: Projection Expressions
// Only fetch fields you need await client.send(new GetCommand({ TableName: Resource.Database.name, Key: { pk: "USER#123", sk: "PROFILE" }, ProjectionExpression: "name, email" // Don't fetch everything }));
Pattern 5: GSI Design
// Sparse indexes save cost const table = new sst.aws.Dynamo("Database", { fields: { pk: "string", sk: "string", gsi1pk: "string", // Only set on items that need indexing gsi1sk: "string" }, primaryIndex: { hashKey: "pk", rangeKey: "sk" }, globalIndexes: { gsi1: { hashKey: "gsi1pk", rangeKey: "gsi1sk", projection: "keys_only" // Cheapest option } } });
// Only active users have gsi1pk { pk: "USER#123", sk: "PROFILE", status: "active", gsi1pk: "ACTIVE#USER", // Only active users gsi1sk: "USER#123" }
S3 Optimization
Pattern 1: Bucket Configuration
const bucket = new sst.aws.Bucket("Uploads", { transform: { bucket: { // Lifecycle rules for cost savings lifecycleConfiguration: { rules: [ { id: "archive-old-files", status: "Enabled", transitions: [ { days: 30, storageClass: "INTELLIGENT_TIERING" }, { days: 90, storageClass: "GLACIER" } ] }, { id: "delete-temp-files", status: "Enabled", expiration: { days: 7 }, filter: { prefix: "temp/" } } ] } } } });
Pattern 2: Intelligent Tiering
// Automatically moves objects between access tiers { storageClass: "INTELLIGENT_TIERING" }
// Tiers: // - Frequent Access (default) // - Infrequent Access (30 days) // - Archive Instant Access (90 days) // - Archive Access (90+ days) // - Deep Archive Access (180+ days)
Pattern 3: CloudFront for Static Assets
const cdn = new sst.aws.Router("CDN", { routes: { "/*": { bucket: bucket } }, transform: { distribution: { defaultCacheBehavior: { compress: true, // Enable compression viewerProtocolPolicy: "redirect-to-https", cachePolicyId: "658327ea-f89d-4fab-a63d-7e88639e58f6" // CachingOptimized } } } });
Pattern 4: Presigned URLs
import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; import { GetObjectCommand } from "@aws-sdk/client-s3";
// Generate time-limited URL const url = await getSignedUrl( s3Client, new GetObjectCommand({ Bucket: Resource.Uploads.name, Key: fileKey }), { expiresIn: 3600 } // 1 hour );
// No Lambda invocation needed for downloads
API Gateway Optimization
Pattern 1: HTTP API vs REST API
// Use HTTP API (cheaper, faster) new sst.aws.ApiGatewayV2("Api", { routes: { "GET /posts": "src/posts.list", "POST /posts": "src/posts.create" } });
// HTTP API is 71% cheaper than REST API // Same features for most use cases
Pattern 2: Response Caching
new sst.aws.ApiGatewayV2("Api", { routes: { "GET /posts": { function: "src/posts.list", // Cache at API Gateway level cache: { ttl: "5 minutes" } } } });
Pattern 3: Request Validation
// Reject invalid requests early (before Lambda invocation) new sst.aws.ApiGatewayV2("Api", { routes: { "POST /posts": { function: "src/posts.create", authorizer: "iam", // Validate request before invoking Lambda } } });
Remix Optimization with SST
Pattern 1: Server Bundle Optimization
// remix.config.js export default { serverBuildPath: "build/server/index.mjs", serverMinify: true, serverModuleFormat: "esm", // Don't bundle Node.js built-ins serverDependenciesToBundle: [ /^(?!node:)/, // Bundle everything except node: imports ] };
Pattern 2: Asset Optimization
const remix = new sst.aws.Remix("Web", { environment: { ASSET_URL: cdn.url // Serve assets from CloudFront }, transform: { server: { // Optimize Lambda memory: "512 MB", architecture: "arm64" } } });
Pattern 3: Edge Caching
// Use CloudFront for edge caching export const headers = () => ({ "Cache-Control": "public, max-age=3600, s-maxage=86400" });
// Cache at edge for 24 hours // Browser cache for 1 hour
Cost Monitoring
Pattern 1: Resource Tagging
new sst.aws.Function("Api", { handler: "src/api.handler", transform: { function: { tags: { Environment: $app.stage, Service: "api", CostCenter: "engineering" } } } });
Pattern 2: Budget Alerts
// Use AWS Budgets to track costs // Set up alerts when approaching limits // Review CloudWatch metrics regularly
Pattern 3: Cost Allocation
// Tag all resources consistently const tags = { Project: "my-app", Environment: $app.stage, Team: "engineering" };
// Apply to all resources new sst.aws.Function("Api", { transform: { function: { tags } } });
Performance Monitoring
Pattern 1: X-Ray Tracing
new sst.aws.Function("Api", { handler: "src/api.handler", transform: { function: { tracingConfig: { mode: "Active" // Enable X-Ray tracing } } } });
// In code import { captureAWS } from "aws-xray-sdk-core"; import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
const client = captureAWS(new DynamoDBClient({}));
Pattern 2: CloudWatch Metrics
import { CloudWatchClient, PutMetricDataCommand } from "@aws-sdk/client-cloudwatch";
const cloudwatch = new CloudWatchClient({});
await cloudwatch.send(new PutMetricDataCommand({ Namespace: "MyApp", MetricData: [{ MetricName: "ProcessingTime", Value: duration, Unit: "Milliseconds" }] }));
Environment-Specific Optimization
Pattern 1: Development Environment
if ($app.stage === "dev") { // Smaller, cheaper resources for dev new sst.aws.Function("Api", { memory: "256 MB", timeout: "10 seconds" }); }
Pattern 2: Production Environment
if ($app.stage === "production") { new sst.aws.Function("Api", { memory: "1024 MB", // More resources timeout: "30 seconds", transform: { function: { reservedConcurrentExecutions: 10, pointInTimeRecovery: { enabled: true } } } }); }
Best Practices Checklist
Lambda Functions
-
Use ARM64 architecture
-
Minimize bundle size
-
Reuse connections
-
Set appropriate memory
-
Set appropriate timeout
-
Use environment variables for config
DynamoDB
-
Use single table design
-
Query instead of Scan
-
Use batch operations
-
Design GSIs carefully
-
Enable TTL for expiring data
-
Use projection expressions
S3
-
Set lifecycle policies
-
Use Intelligent Tiering
-
Enable CloudFront for static assets
-
Use presigned URLs
-
Compress files before upload
API Gateway
-
Use HTTP API over REST API
-
Enable response caching
-
Validate requests early
-
Use custom domains
Monitoring
-
Tag all resources
-
Set up CloudWatch alarms
-
Enable X-Ray tracing
-
Review Cost Explorer monthly
-
Set budget alerts
Cost Optimization Strategies
- Right-Size Resources
Monitor and adjust:
Check Lambda memory usage
If max memory used < 60% of allocated, reduce
- Use Reserved Capacity
For predictable workloads:
-
DynamoDB Reserved Capacity
-
Lambda Provisioned Concurrency
-
Savings Plans
- Cleanup Unused Resources
Regular audit
sst remove --stage old-feature
- Optimize Data Transfer
-
Use CloudFront for global distribution
-
Keep data in same region
-
Use VPC endpoints for AWS services
Common Anti-Patterns
❌ Don't:
-
Over-provision memory "just in case"
-
Use Scan on large tables
-
Keep all data forever
-
Ignore CloudWatch metrics
-
Deploy to multiple regions unnecessarily
-
Use REST API when HTTP API works
✅ Do:
-
Start small, scale based on metrics
-
Use Query with partition keys
-
Set up lifecycle policies
-
Monitor and optimize regularly
-
Deploy to one region initially
-
Use HTTP API by default
Further Reading
-
AWS Well-Architected Framework: https://aws.amazon.com/architecture/well-architected/
-
AWS Cost Optimization: https://aws.amazon.com/pricing/cost-optimization/
-
Lambda Power Tuning: https://github.com/alexcasalboni/aws-lambda-power-tuning
-
SST Docs: https://sst.dev/docs