Cutting AWS Fargate cold starts in half
A practical walk through layered images, provisioned capacity, and the unsung win of moving heavy imports behind lazy boundaries.
Niko Petrov
Cloud Architect
Our worst endpoint took 9.4 seconds to first byte — but only after a deploy, only sometimes, and never in the demo. Classic cold start. Fargate gives you clean isolation and a quiet bill, and in exchange it makes you pay for container pulls and process boot on every scale-out. We got the p99 from 9.4s to 4.1s without changing a single business rule. Here is the itemized receipt.
Weigh the image before you tune anything
Cold start begins with the image pull, and the pull is dominated by bytes. Ours was 1.9 GB because the build image and the runtime image were the same image. A multi-stage build with a distroless runtime cut it to 280 MB — and layer ordering did almost as much, because layers that never change get served from the pull-through cache.
FROM node:22-slim AS build
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable && pnpm i --frozen-lockfile
COPY . .
RUN pnpm build && pnpm prune --prod
FROM gcr.io/distroless/nodejs22
WORKDIR /app
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
CMD ["dist/server.js"]
Keep a warm floor, scale on the real signal
Scale-to-zero is a hobby setting for anything customer-facing. We keep a minimum of two tasks per service — the floor costs less per month than one incident retro costs in salaries — and we scale on ALBRequestCountPerTarget instead of CPU, because request count moves seconds earlier. Step scaling adds two tasks at a time; by the time CPU notices, the new tasks are already draining the queue.
- Image: 1.9 GB → 280 MB. Pull time: 41s → 6s.
- Boot: lazy imports moved the SDK tax off the critical path — 3.1s → 0.9s to first listen.
- Floor of two tasks: cold paths only exist during deploys now, and deploys pre-warm.
The unsung win: lazy boundaries
The quietest 2.2 seconds we recovered were import-time. The AWS SDK, the PDF renderer, and the spreadsheet parser were all loaded at boot for routes that see five requests a day. Moving them behind dynamic import() boundaries means the process listens first and pays for heavy modules when — if — they are actually hit.
Measure before you believe
Wrap your entrypoint imports in timers once and look at the numbers. Import-time is the cold-start cost nobody profiles, because profilers start after it already happened.
Cold starts are not one problem. They are four small bills arriving in the same envelope.
Niko Petrov
Cloud Architect
AWS Advanced Tier lead. Optimizes for the bill you see and the latency you don’t.
READ NEXT
-
Uncategorized
Hello world!
Welcome to WordPress. This is your first post. Edit or delete it, then start writing!
· Jul 17, 2026
-
Engineering
Why we run on monorepos again
After three years of polyrepos, the cost of context switching outgrew the cost of one big lockfile. Here is what we kept, what we changed, and the tooling that finally made it work.
Anders Lindqvist · May 28, 2026
-
Design
Design tokens that survive engineering handoff
Most token systems break the moment a developer needs a hover-active-disabled triplet. A short field guide to naming, composition, and the four rules we never break.
Mira Holmberg · May 19, 2026