Lambda functions using Node.js often balloon in size, making cold starts slow and deployments painful.

Let’s look at a simple Lambda function and how esbuild can shrink it.

index.js:

import express from 'express';

const app = express();
const port = 3000;

app.get('/', (req, res) => {
  res.send('Hello World!');
});

app.listen(port, () => {
  console.log(`Example app listening on port ${port}`);
});

If we were to just npm install express and zip this up, the package size would be significant, mostly due to express and its dependencies.

Now, let’s see how esbuild can help us bundle this more efficiently.

First, install esbuild as a dev dependency:

npm install --save-dev esbuild

Then, create a build script in your package.json:

{
  "scripts": {
    "build": "esbuild index.js --bundle --outfile=bundle.js --platform=node --format=cjs --external:express"
  }
}

Let’s break down these esbuild options:

  • index.js: This is our entry point.
  • --bundle: This is the magic. esbuild will analyze your code and include all necessary dependencies.
  • --outfile=bundle.js: The output file name.
  • --platform=node: Tells esbuild to target a Node.js environment. This ensures it uses Node.js-specific APIs and doesn’t try to bundle browser-specific polyfills.
  • --format=cjs: Generates CommonJS output, which is what Node.js typically uses.
  • --external:express: This is crucial for minimizing package size. It tells esbuild not to bundle the express package. Instead, it assumes express will be available in the Lambda runtime environment. Node.js runtimes in Lambda do include express by default (or you can easily include it in your node_modules if you need a specific version).

Run the build script:

npm run build

This will create a bundle.js file. If you inspect this file, you’ll notice it’s much smaller than the original node_modules directory would have been. The bundle.js contains your application code and any other dependencies that were not marked as external.

To deploy this to Lambda, you’d typically zip bundle.js and upload it. For a Lambda function that uses express, you’d usually configure the handler to point to the bundle.js file, and the runtime would handle loading express if it’s a managed dependency or if you’ve explicitly included it.

The mental model here is that esbuild acts as a highly optimized bundler. It understands JavaScript syntax (including ES Modules) and efficiently traverses your dependency graph. By marking large, common dependencies as external, you leverage the pre-existing runtime environment and drastically reduce the size of your deployment package. This means faster uploads, quicker unzipping by Lambda, and ultimately, faster cold starts.

A common pitfall is forgetting to mark all large, common dependencies as external. If you have multiple libraries that are already present in the Lambda Node.js runtime (like aws-sdk, crypto, etc.), and you don’t explicitly mark them as external, esbuild might bundle them unnecessarily, increasing your package size. You’ll want to check the specific Node.js runtime version you’re using in Lambda to see what’s pre-installed.

The next step is often optimizing your Lambda configuration for performance, such as adjusting memory allocation or provisioned concurrency.

Want structured learning?

Take the full Lambda course →