All posts
Cloud May 12, 2026 11 min read

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.

N

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.

N

Niko Petrov

Cloud Architect

AWS Advanced Tier lead. Optimizes for the bill you see and the latency you don’t.