Reduce AWS SDK v3 Bundle Size and Cold Starts in Lambda

Lines of colorful source code on a dark code editor screen

Photo by Mohammad Rahmani on Unsplash

To reduce AWS SDK v3 Lambda bundle size, bundle the handler with esbuild using bundle, minify, platform: "node", format: "esm" and mainFields: ["module", "main"], import only the clients and commands you call, and deploy the single output file instead of node_modules. In our test, a DynamoDB handler went from 7.06 MiB of dependencies to a 321.2 KiB bundle.

Most advice on the aws sdk v3 lambda bundle size is a list of settings with no numbers. This guide is for Node.js and TypeScript developers deploying Lambda functions who want to know which settings actually matter. You’ll get the handler and the esbuild script we used, the sizes we measured for each variant, a runtime error you’ll hit with ESM output and its fix, and a CloudWatch Logs Insights query to track cold starts in your own account.

All numbers below were measured on 28 September 2026 with @aws-sdk/client-dynamodb and @aws-sdk/lib-dynamodb 3.1141.0, esbuild 0.28.2 and Node.js 24.15.0. Every bundle was run end to end against a local fake DynamoDB endpoint to confirm it works. They weren’t measured inside Lambda, so treat the load-time figures as a relative guide.

Should you use the SDK in the Lambda runtime or bundle your own?

Every supported Lambda Node.js runtime (nodejs22.x, nodejs24.x and nodejs26.x as of September 2026) includes a specific minor version of AWS SDK for JavaScript v3, which depends on the runtime version and Region. You can mark @aws-sdk/* as external and ship almost nothing:

Runtime-included SDK Bundled SDK
Your deployment package Your code only (0.5 KiB in our test) Your code plus the SDK code you use (321.2 KiB)
SDK version Chosen by AWS, can change with runtime updates Pinned by your lockfile
Newest APIs and fixes Only when the runtime catches up As soon as you upgrade
Same code in tests and production No Yes

The Lambda Node.js handler docs strongly recommend including the SDK clients you need in the deployment package, for compatibility with future runtime updates, and relying on the runtime copy only when you can’t add packages. We follow that here. If you still ship aws-sdk v2, start with migrating a Node.js app from AWS SDK v2 to v3; no supported Node.js runtime includes v2.

Prerequisites

  • Node.js 18 or later locally, and a function on nodejs22.x or later with the handler set to index.handler.
  • esbuild, typescript and @types/node as dev dependencies, and the SDK clients your handler imports as dependencies.
  • Handlers already written against SDK v3 commands. For v2 files, the free AWS SDK JavaScript v2 to v3 converter produces a first draft to review.

How to cut your AWS SDK v3 Lambda bundle size, step by step

  1. Import clients, not the whole SDKInstall one @aws-sdk/client-* package per service and import the client class and the commands you send.
  2. Bundle with esbuildSet bundle: true, platform: "node", a target that matches your runtime and format: "esm".
  3. Prefer the ES module buildsSet mainFields: ["module", "main"]. For platform: "node" esbuild’s default is main,module, which picks the CommonJS builds that tree-shake poorly.
  4. Minifyminify: true shrinks identifiers and whitespace.
  5. Add a require shim to ESM outputA one-line banner that defines require with createRequire, explained below.
  6. Check what went inWrite the metafile and print analyzeMetafile to see the largest inputs.
  7. Measure Init DurationCompare the REPORT lines before and after you deploy.

Example: the handler and the esbuild build script

The test handler reads one item with the DynamoDB document client, the same pattern as in querying DynamoDB with AWS SDK v3:

src/handler.ts

// src/handler.ts: reads one order from DynamoDB by id.
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient, GetCommand } from "@aws-sdk/lib-dynamodb";

// Created once per execution environment, during Init, and reused by every invocation.
const ddb = DynamoDBDocumentClient.from(new DynamoDBClient({}));
const TABLE = process.env.TABLE_NAME ?? "orders";

interface OrderEvent {
  orderId: string;
}

export const handler = async (event: OrderEvent): Promise<{ statusCode: number; body: string }> => {
  const { Item } = await ddb.send(new GetCommand({ TableName: TABLE, Key: { pk: event.orderId } }));
  return Item
    ? { statusCode: 200, body: JSON.stringify(Item) }
    : { statusCode: 404, body: JSON.stringify({ message: "not found" }) };
};
build.mjs

// build.mjs: bundle a Lambda handler with esbuild and report what went into it.
// Usage: node build.mjs [--external-sdk]
import { build, analyzeMetafile } from "esbuild";
import { writeFileSync } from "node:fs";

const externalSdk = process.argv.includes("--external-sdk");

const result = await build({
  entryPoints: ["src/handler.ts"],
  outfile: "dist/index.mjs",
  bundle: true,
  minify: true,
  sourcemap: "linked",
  platform: "node",
  target: "node22",
  format: "esm",
  // Prefer each package's ES module build, which esbuild can tree-shake.
  mainFields: ["module", "main"],
  // Leave the SDK out and use the copy in the Lambda runtime, if you choose to.
  external: externalSdk ? ["@aws-sdk/*"] : [],
  // Some bundled CommonJS code calls require("node:https"); give ESM output a real require.
  banner: { js: "import { createRequire } from 'node:module'; const require = createRequire(import.meta.url);" },
  metafile: true,
  logLevel: "info",
});

writeFileSync("dist/meta.json", JSON.stringify(result.metafile));
console.log(await analyzeMetafile(result.metafile, { verbose: false }));
Terminal

npm install @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb
npm install --save-dev esbuild typescript @types/node

# Bundle, then zip only the output (no node_modules)
node build.mjs
cd dist && zip -q ../function.zip index.mjs && cd ..

# Deploy: runtime nodejs22.x or later, handler "index.handler"
aws lambda update-function-code --function-name orders-get --zip-file fileb://function.zip

The esbuild API documentation describes mainFields, external wildcards and the metafile in detail.

What did each setting change? Our measurements

Sizes of the single output file, and of that file zipped with deflate, for the same handler unless noted:

Build Output Zipped
No bundling: handler plus node_modules for the two SDK packages 7.06 MiB in about 3,200 files 2.61 MiB
esbuild, minified, default mainFields 513.4 KiB 151.0 KiB
esbuild, minified, mainFields: ["module", "main"] 321.2 KiB 99.5 KiB
Same, not minified 817.8 KiB 157.2 KiB
@aws-sdk/* external (runtime SDK) 0.5 KiB 0.4 KiB
Aggregated DynamoDBDocument class instead of DynamoDBDocumentClient plus GetCommand 368.6 KiB 111.6 KiB
DynamoDBClient plus GetItemCommand, no document client 309.9 KiB 96.0 KiB
Aggregated DynamoDB class with getItem 350.2 KiB 108.3 KiB
S3Client plus HeadObjectCommand 394.4 KiB 120.8 KiB
DynamoDB document client and S3 in one handler 445.4 KiB 134.3 KiB
For comparison: SDK v2, aws-sdk/clients/dynamodb only (2.1693.0) 373.5 KiB 97.7 KiB
For comparison: SDK v2, whole aws-sdk import 9,538.8 KiB 1,195.6 KiB

Three results stood out. The mainFields setting mattered most, cutting 37% on its own. With the ES module builds, the command style finally pays: the aggregated classes added 40 to 47 KiB, while with the default mainFields the aggregated and command styles came out within a few dozen bytes of each other. And a v2 single-client import was only about 16% larger than our best v3 DynamoDB bundle, so the big gap is against the whole v2 SDK, not against a careful v2 import.

The metafile shows where the rest goes. In our 321.2 KiB bundle, @smithy/core contributed 108.7 KiB, @aws-sdk/nested-clients (the STS and SSO clients the credential providers use) 53.9 KiB, @aws-sdk/core 48.8 KiB and @aws-sdk/client-dynamodb itself only 26.1 KiB. That shared core is why a second client costs less than the first: adding S3 to the DynamoDB handler added 124.2 KiB, not the 394.4 KiB an S3-only bundle weighs.

Why does an ESM bundle fail with “Dynamic require is not supported”?

Parts of the SDK dependency tree are CommonJS and call require() for Node.js built-ins. esbuild keeps those calls, but an ES module has no require. Without the banner, importing our bundle from an ES module threw during initialization:

Error without the banner

Error: Dynamic require of "node:https" is not supported

The banner defines require at the top of the file with Node.js module.createRequire, which fixes it. A quick node -e "import('./dist/index.mjs')" check didn’t show the problem, because node -e provides a global require; only loading it from a real .mjs file did. Invoke every bundle once from an ES module before you ship it. Alternatively, emit format: "cjs" to an index.js file and skip the banner.

How do you measure cold starts in Lambda?

Every invocation writes a REPORT line to the function’s log group, the same one you read to investigate Lambda errors with CloudWatch. On the first request an execution environment serves, it includes Init Duration: the time the runtime took to load the function and run code outside the handler. That’s the number bundle size affects. With the default text log format, this Logs Insights query summarizes it per day:

Cold starts per day (Logs Insights)

filter @type = "REPORT"
| parse @message /Init Duration: (?<initMs>[\d.]+) ms/
| stats count(*) as invocations, count(initMs) as coldStarts, avg(initMs) as avgInitMs, pct(initMs, 95) as p95InitMs, max(initMs) as maxInitMs by bin(1d)

Run it for a week before and after the change, from the console or with the helper in running CloudWatch Logs Insights queries with AWS SDK v3. With the JSON log format, the report arrives as a structured platform event rather than a text line, so adjust the filter and field names.

As a local proxy, we timed a cold import() of each variant in a fresh Node.js 24.15.0 process on a Windows 11 laptop, 25 runs each. Medians: 34.8 ms for the module,main bundle, 40.7 ms for the default-mainFields bundle and 52.3 ms for the unbundled handler loading from node_modules (an empty module took 1.9 ms). Lambda’s numbers will differ; the ordering is what to expect, and Init Duration in your own logs is the number to trust.

What else lowers Init Duration?

  • Create clients outside the handler. The Lambda docs recommend it: the client is built once during Init and reused by later invocations in the same environment. Creating clients inside the handler repeats that work on every request.
  • Don’t disable keep-alive. It’s on by default in all supported Node.js runtimes, which saves a new TCP and TLS connection per SDK call. Set sensible timeouts too, as in AWS SDK v3 retries and timeouts in Lambda.
  • Defer rarely used clients. A dynamic import() inside the branch that needs a client delays running its module code until that branch. Without code splitting, esbuild still puts that code in the same file, so check the metafile.
  • Revisit memory. Lambda allocates CPU in proportion to memory, which can change Init time too. Finding Lambda functions with too much memory shows the other side of that trade-off, and finding functions not yet on arm64 covers the architecture switch; a pure JavaScript bundle like this one runs unchanged on both.

Common mistakes and how to fix them

  • Bundling but still zipping node_modules. The bundle already contains the SDK. Zip only dist/index.mjs.
  • Marking @aws-sdk/* external without meaning to. Some build tools do this by default for newer runtimes; check your config. Your code then runs against the runtime’s SDK version, not the one you tested.
  • Handler not found after switching to ESM. The file must be index.mjs, or index.js with "type": "module" in a package.json shipped alongside it.
  • A target newer than the runtime. Keep esbuild’s target at or below the runtime’s Node.js version. Upgrade old functions first; find Lambda functions on deprecated runtimes lists them.
  • Unit tests that import the bundle. Test the source instead and mock AWS SDK v3 clients in Jest or Vitest; mocks patch the SDK in node_modules, not the copy inside a bundle.

Limits of bundle size tuning

  • Our numbers are for one small handler and one SDK version. Your bundle will differ; measure it with the metafile.
  • A smaller bundle shortens loading, not your own initialization work such as fetching secrets or opening database connections.
  • Cold starts only affect requests that land on a new execution environment. For latency-critical paths, provisioned concurrency removes them at a price. The script to find unused Lambda provisioned concurrency shows when that price stops paying off.
  • Bundling doesn’t reduce IAM or runtime risks. Old function versions still count toward your code storage, so delete old and unused Lambda function versions after you redeploy.

To check how your functions are configured today without writing a script, ChatWithCloud answers questions such as “Which Lambda functions run nodejs22.x and how much memory do they have?” from your terminal; how ChatWithCloud runs read-only AWS SDK queries explains what it sends and where the code runs.

Frequently asked questions

Does the Lambda Node.js runtime include AWS SDK v3?

Yes. All supported Node.js runtimes include a specific minor version of SDK v3, which depends on the runtime and Region. The Lambda docs still recommend bundling the clients you use so runtime updates can’t change them under you.

Should I mark @aws-sdk as external in esbuild?

Only if you accept whichever SDK version the runtime has. It gives the smallest package, 0.5 KiB in our test, but your tested and deployed SDK versions can drift apart.

Is AWS SDK v3 smaller than v2 in Lambda?

Much smaller than importing all of v2 (9.3 MiB bundled in our test), but a v2 single-client import was 373.5 KiB against 321.2 KiB for our v3 DynamoDB bundle. The bigger reasons to move are that v2 reached end of support and isn’t in any supported Node.js runtime.

Where do I see Lambda cold start time?

In the Init Duration field of the REPORT line in the function’s CloudWatch log group. It appears only for the first request an execution environment serves.

Related guides

Ask your AWS account in plain English

Your first 15 runs are free, with no OpenAI key needed.

npx chatwithcloud