A deploy in one of my CDK projects passed every gate I have — CI, cdk synth, code review — and merged. The CloudFormation stack deployment to AWS failed with:
InvalidRequest: Unzipped size must be smaller
than 262144000 bytes
262,144,000 bytes is 250 MiB, AWS's hard limit on the unzipped deployment package of a zip-packaged Lambda. A few transitive dependencies had crept in and pushed one function's bundle over the edge.
It failed in an uncomfortable place. cdk synth builds the CloudFormation template and stages every Lambda's code asset under cdk.out/asset.<hash>/, but it never weighs the unzipped size of what it staged. AWS enforces unzipped size at deploy time, and nothing in the pipeline measured it. Synth, review, and merge were all blind to it, and the failure surfaced only after everything that was supposed to catch problems had already said fine.
What it measures
cdk-lambda-size-gate runs in CI right after cdk synth and reads the finished cloud assembly rather than guessing from source. cdk.out is a language-agnostic artifact — a TypeScript CDK app, a Python one, Java, C#, and Go all synth to the same structure: *.template.json CloudFormation, *.assets.json manifests, and asset.<hash>/ staged directories. Reading that output reconstructs exactly how AWS accounts for the unzipped package:
- Walk every
*.template.jsonincdk.outand select theAWS::Lambda::Functionresources. Skip any withPackageType: Image— container Lambdas get a 10 GB limit, not 250 MiB, so they're exempt. - Pull each function's
Code.S3Key, resolve the staged asset directory through the stack's*.assets.jsonmanifest, and fall back to the conventionalasset.<hash>/directory if the manifest lookup misses. - Sum the on-disk size of every file in that directory. Symlinks are counted by their own entry and never followed, so a self-referential link can't loop or double-count.
Asset sizes are cached, since several functions can share one bundle — two Lambdas built from the same source stage the same asset, and each is reported against the shared size.
The thresholds map to what AWS enforces. Strictly above 250 MiB fails the build, since the deploy would be rejected. Above 200 MiB but within the limit is a warning — headroom is running low and a couple more dependencies will push it over. At or below 200 MiB passes.
Function plus layers
AWS's 250 MiB limit is the size of the deployment package contents, including layers and custom runtimes — the function's own code plus every layer it references, combined. A function can pass a check that only weighs its own asset and still die on deploy because function + layers crossed the line.
The gate resolves each function's Layers property to the AWS::Lambda::LayerVersion assets and computes the total per function — own asset plus every referenced layer asset. A shared layer counts against every function that uses it. Layers referenced by a bare ARN (an AWS-managed layer, or one from another account) aren't staged in cdk.out and can't be weighed, so a function that uses one is reported with a ≥ marker to flag that the total is a lower bound.
Using it
The gate ships as a composite GitHub Action. A single step after synth covers it:
- run: npx cdk synth --quiet
- name: Lambda bundle-size gate
uses: schuettc/cdk-lambda-size-gate@v1
with:
cdk-out: cdk.out
The output is a per-function table with the total broken into function and layer bytes, plus GitHub Actions ::warning:: and ::error:: annotations so oversized functions surface inline on the PR:
STATUS TOTAL (MiB) FN (MiB) LYR (MiB) STACK FUNCTION ASSET
FAIL 250.0 250.0 0.0 Demo FnFail fn-fail
WARN 209.8 209.8 0.0 Demo FnWarn fn-warn
OK 0.0 0.0 0.0 Demo FnOk fn-ok
Exit codes drive the gate: 1 if any function exceeds the hard limit, 2 on a usage error or a missing cdk.out, 0 otherwise. The hard and warn limits are inputs measured in bytes, defaulting to 262,144,000 (250 MiB) and 209,715,200 (200 MiB), so a project with its own headroom policy can lower them:
- uses: schuettc/cdk-lambda-size-gate@v1
with:
cdk-out: cdk.out
hard-limit: "251658240" # 240 MiB
warn-limit: "188743680" # 180 MiB
The same binary runs locally and in GitLab, CircleCI, or Jenkins — the Action is a thin wrapper that downloads it and runs it against cdk.out.
Why a static binary
Because cdk.out is language-agnostic, the language of the checker is decoupled from the language of the app being checked. Reading JSON and summing file sizes is something any language does, so the choice comes down to distribution: how does a user on any CI runner, in any ecosystem, run this with the least friction?
A single static binary, which needs nothing installed on the runner, answers that. The tool is written in Go: the whole job is read JSON, walk directories, sum sizes, print a table, exit — all of which the Go standard library covers with no third-party dependencies. goreleaser emits binaries for linux, macOS, and Windows across amd64 and arm64 in one release, and the composite Action downloads the right one for the runner. A static binary has no runtime dependency at all — it runs on a distroless image or a FROM scratch container the same as on a full runner, no language toolchain required on the CI image.
The tool is public at github.com/schuettc/cdk-lambda-size-gate and listed on the GitHub Marketplace