Fix TTFB on AWS Amplify: Optimize Time to First Byte
AWS Amplify Hosting provides a fully managed CI/CD and hosting platform backed by Amazon CloudFront, one of the largest CDN networks with over 400 edge locations. Despite this powerful infrastructure, TTFB problems are common due to Lambda cold starts for SSR routes, misconfigured CloudFront cache behaviors, and the default lack of edge compute for dynamic content.
Amplify Gen 2 introduced significant improvements: CDK-based infrastructure, better Next.js support with ISR caching, and more granular control over CloudFront configurations. However, many Amplify projects still use suboptimal defaults that result in TTFB of 1-3 seconds for server-rendered pages.
This guide covers five AWS Amplify-specific optimizations: CloudFront caching behaviors, Lambda@Edge and CloudFront Functions, SSR compute tuning, response caching strategies, and monitoring with CloudWatch. These techniques bring Amplify TTFB from 1.8s to under 100ms for most pages.
Expected results
Following all steps in this guide typically produces these improvements:
Before
1.8s
TTFB (Poor) -- SSR Lambda with cold starts, no CloudFront caching for HTML pages
After
85ms
TTFB (Good) -- CloudFront cached HTML with Origin Shield and optimized Lambda compute
Step-by-step fix
Configure CloudFront caching behaviors
CloudFront's caching layer is the most impactful TTFB optimization on Amplify. By default, Amplify configures CloudFront to forward most requests to the origin (your Lambda or S3 bucket). Proper cache policies eliminate origin round trips for the vast majority of requests, serving HTML directly from edge nodes in 10-30ms.
// app/blog/[slug]/page.tsx
// ISR on Amplify: cached at CloudFront edge
export const revalidate = 3600; // Revalidate every hour
export default async function BlogPost({ params }) {
const post = await getPost(params.slug);
return <Article post={post} />;
}
// Amplify Gen 2 translates revalidate to:
// Cache-Control: public, s-maxage=3600, stale-while-revalidate=86400
// CloudFront caches the response for 1 hour
// Serves stale content while revalidating in background
// next.config.js -- explicit headers for non-ISR pages
module.exports = {
async headers() {
return [{
source: '/docs/:path*',
headers: [{
key: 'Cache-Control',
value: 'public, s-maxage=86400, stale-while-revalidate=604800',
}],
}];
},
};
// amplify/backend.ts
import { defineBackend } from '@aws-amplify/backend';
import * as cloudfront from 'aws-cdk-lib/aws-cloudfront';
const backend = defineBackend({});
// Customize CloudFront cache behavior for SSR routes
const cfDistribution = backend.getConstruct('ServerCdnDistribution');
// Enable Origin Shield in the region closest to your origin
// Reduces origin requests by 60-90%
cfDistribution.node.defaultChild.addPropertyOverride(
'DistributionConfig.Origins.0.OriginShield',
{
Enabled: true,
OriginShieldRegion: 'us-east-1',
}
);
Use CloudFront Functions for edge logic
CloudFront Functions execute in 200+ edge locations with sub-1ms execution time -- far faster than Lambda@Edge (which runs in 13 regional locations). Use CloudFront Functions for URL rewrites, redirects, header manipulation, and simple A/B testing. They run on viewer request/response events and can prevent unnecessary origin requests entirely.
// CloudFront Function: normalize URLs for better cache hit rates
function handler(event) {
var request = event.request;
var uri = request.uri;
// Add trailing slash (normalizes /blog and /blog/ to same cache key)
if (uri.length > 1 && !uri.endsWith('/') && !uri.includes('.')) {
return {
statusCode: 301,
statusDescription: 'Moved Permanently',
headers: {
location: { value: uri + '/' },
'cache-control': { value: 'public, max-age=3600' },
},
};
}
// Add index.html for directory requests
if (uri.endsWith('/')) {
request.uri += 'index.html';
}
// Remove marketing query params (improve cache key)
var params = request.querystring;
delete params.utm_source;
delete params.utm_medium;
delete params.utm_campaign;
delete params.fbclid;
delete params.gclid;
return request;
}
// Execution: <1ms at edge
// Impact: Normalizes URLs for 20-40% higher cache hit rate
Optimize SSR Lambda compute settings
Amplify SSR routes run on AWS Lambda. Cold starts are the primary TTFB bottleneck for uncached SSR pages. Three optimizations help: increasing memory allocation (which also increases CPU), reducing bundle size, and enabling provisioned concurrency for high-traffic routes.
// amplify/backend.ts
import { defineBackend } from '@aws-amplify/backend';
const backend = defineBackend({});
// Increase Lambda memory (also increases CPU proportionally)
// 512MB = 0.5 vCPU, 1024MB = 1 vCPU, 1769MB = 1 full vCPU
const ssrFunction = backend.getConstruct('ServerFunction');
ssrFunction.node.defaultChild.addPropertyOverride('MemorySize', 1024);
ssrFunction.node.defaultChild.addPropertyOverride('Timeout', 10);
// Memory comparison for SSR execution:
// 128MB: ~800ms cold start, ~400ms warm execution
// 512MB: ~400ms cold start, ~150ms warm execution
// 1024MB: ~250ms cold start, ~80ms warm execution
// 1769MB: ~180ms cold start, ~50ms warm execution
// Provisioned concurrency: keep N instances warm (eliminates cold starts)
// Note: this costs more -- only for critical high-traffic routes
// Configure via CDK override on the Lambda function
// next.config.js -- optimized for Amplify
module.exports = {
output: 'standalone', // Produces minimal server bundle
experimental: {
// Externalize large packages from the Lambda bundle
serverComponentsExternalPackages: ['sharp', '@aws-sdk/client-s3'],
},
// Enable SWC minification (faster builds, smaller output)
swcMinify: true,
// Reduce image processing overhead
images: {
formats: ['image/avif', 'image/webp'],
minimumCacheTTL: 86400,
},
};
// Bundle size impact on cold starts:
// 5MB bundle: ~300ms download + init
// 15MB bundle: ~800ms download + init
// 50MB bundle: ~2000ms download + init
// Target: < 10MB for sub-500ms cold starts
Implement response caching strategies
Beyond CloudFront caching, you can implement application-level caching within your Lambda functions. This reduces execution time for warm invocations by caching database results, API responses, and rendered HTML fragments in memory across invocations.
// lib/cache.ts
// Module-level cache persists across warm Lambda invocations
const cache = new Map<string, { data: any; expiry: number }>();
export function getCached<T>(key: string): T | null {
const entry = cache.get(key);
if (!entry) return null;
if (Date.now() > entry.expiry) {
cache.delete(key);
return null;
}
return entry.data as T;
}
export function setCache(key: string, data: any, ttlMs: number) {
cache.set(key, { data, expiry: Date.now() + ttlMs });
// Prevent memory leaks: limit cache size
if (cache.size > 1000) {
const oldestKey = cache.keys().next().value;
cache.delete(oldestKey);
}
}
// Usage in a server component:
export async function getProduct(slug: string) {
const cached = getCached(`product:${slug}`);
if (cached) return cached; // ~0ms
const product = await db.product.findUnique({ where: { slug } });
setCache(`product:${slug}`, product, 60_000); // Cache 60s
return product; // ~50ms (DB query)
}
// Warm invocation with cache hit: ~5ms total
// Warm invocation with cache miss: ~50ms (DB)
// Cold invocation: ~300ms (init) + ~50ms (DB)
Monitor TTFB with CloudWatch
AWS provides detailed metrics through CloudWatch for both CloudFront and Lambda. Set up dashboards and alarms to track TTFB, cache hit rates, cold start frequency, and function duration. This visibility is essential for maintaining fast TTFB as your application evolves.
// amplify/backend.ts -- CloudWatch alarms via CDK
import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch';
// Alarm if Lambda p95 duration exceeds 500ms
new cloudwatch.Alarm(stack, 'SSRLatencyAlarm', {
metric: ssrFunction.metricDuration({ statistic: 'p95' }),
threshold: 500,
evaluationPeriods: 3,
comparisonOperator: cloudwatch.ComparisonOperator
.GREATER_THAN_THRESHOLD,
alarmDescription: 'SSR function p95 latency exceeds 500ms',
});
// Track CloudFront cache hit rate
new cloudwatch.Alarm(stack, 'CacheHitRateAlarm', {
metric: new cloudwatch.Metric({
namespace: 'AWS/CloudFront',
metricName: 'CacheHitRate',
dimensionsMap: { DistributionId: cfDistribution.distributionId },
statistic: 'Average',
}),
threshold: 80, // Alert if cache hit rate drops below 80%
evaluationPeriods: 5,
comparisonOperator: cloudwatch.ComparisonOperator
.LESS_THAN_THRESHOLD,
});
// Key metrics to track:
// - CloudFront CacheHitRate (target: >85%)
// - Lambda Duration p50 and p95
// - Lambda ColdStartDuration
// - OriginLatency (time to fetch from Lambda)
Quick checklist
-
Cache-Control headers set with
s-maxageandstale-while-revalidatefor SSR pages - Origin Shield enabled in your primary AWS region
- CloudFront Functions configured for URL normalization and redirect handling
- Lambda memory set to 1024MB+ for SSR functions
- Function bundle size under 10MB (use standalone output)
- CloudWatch alarms configured for latency and cache hit rate monitoring
Frequently asked questions
Amplify uses CloudFront (400+ edge locations) for CDN delivery, giving it excellent static TTFB. For SSR, Amplify runs Lambda functions which have slower cold starts than Vercel's Edge Runtime but offer more compute power and longer execution time. Properly configured with caching, both platforms achieve sub-100ms TTFB for cached content. Amplify's advantage is tighter AWS integration (DynamoDB, S3, Cognito); Vercel's advantage is simpler developer experience.
Amplify SSR uses AWS Lambda, which requires downloading the function bundle, starting the Node.js runtime, and initializing your application on cold starts. This adds 500-3000ms depending on bundle size and memory allocation. Reduce cold starts by keeping bundles under 10MB, setting memory to 1024MB+ (which also increases CPU), and implementing CloudFront caching to minimize function invocations. For the highest traffic pages, provisioned concurrency eliminates cold starts entirely at additional cost.
Yes. CloudFront Functions run in 200+ edge locations with sub-1ms execution time and are ideal for URL normalization, redirects, header manipulation, and simple A/B testing. In Amplify Gen 2, you can attach CloudFront Functions via CDK overrides on the distribution. They complement SSR by handling lightweight edge logic without invoking Lambda, which improves both TTFB and cost efficiency.
Set Cache-Control headers in your framework's SSR response. Amplify Gen 2 respects these headers and configures CloudFront accordingly. For Next.js, use export const revalidate = 3600 for ISR or set headers explicitly in next.config.js. Use s-maxage for CDN caching and stale-while-revalidate for background revalidation. Enable Origin Shield to further reduce cache misses at the origin.
Yes, significantly. Amplify Gen 2 uses a CDK-based architecture that gives you direct control over CloudFront cache behaviors, Lambda memory settings, and Origin Shield configuration. Gen 2 properly supports Next.js App Router with ISR caching (Gen 1 had known issues with ISR). Gen 2 also produces smaller Lambda bundles, reducing cold start time by 30-50% compared to Gen 1.
Related resources
Complete TTFB Guide
Deep dive into Time to First Byte -- thresholds, measurement, and optimization.
FixFix TTFB on Vercel
Vercel-specific TTFB optimizations with ISR and Edge Runtime.
FixCDN Optimization for LCP
CDN configuration strategies for faster content delivery.
FixEdge Functions for TTFB
How edge computing achieves sub-100ms TTFB globally.