← Back to list

Supercharge Your Serverless Deployments with serverless-esbuild Plugin

If you’ve built anything serious on AWS Lambda with the Serverless Framework, you’ve felt it: that long pause after you hit serverless…

Manish Prasad · 2026-06-08 15:31 · 0 claps · 6.8 min read
#serverless #serverless-framework #aws-lambda #esbuild #aws-lambda-functions
Open on Medium ↗
Wiki topics: ☁️ · DevOps & Cloud 🔧 · Data Engineering

Supercharge Your Serverless Deployments with serverless-esbuild Plugin

If you’ve built anything serious on AWS Lambda with the Serverless Framework, you’ve felt it: that long pause after you hit **serverless deploy. For years, `serverless-webpack`** was the default answer to "how do I bundle my functions?" It worked but it was slow and configuring it felt like a part-time job.

Enter serverless-esbuild. It is a Serverless Framework plugin that replaces the older bundling pipeline with **esbuild, a bundler written in Go. esbuild** benchmarks at roughly 10 to 100 times the speed of the JavaScript bundlers many projects still run. The practical results are faster CI, faster local iteration and less waiting.

This post covers what the plugin does, why it matters and how to set it up. By the end you will have a working configuration with TypeScript, shared utilities and a deploy.

The Problem With the Default Serverless Workflow

The Serverless Framework handles infrastructure as code well. But the moment you run serverless deploy with anything beyond a single handler.js file, you hit several walls:

  • You have to configure a bundler yourself: Webpack, Rollup, Parcel, or esbuild.
  • Webpack-based plugins like serverless-webpack add 30 to 120 seconds to every deploy.
  • TypeScript, JSX, modern syntax and path aliases each need extra configuration.
  • Watching files locally is slow because every change triggers a full re-bundle.
  • Cold starts get worse when bundles carry unused code.

On a small project this is an annoyance. On a monorepo with 30 Lambda functions it costs real time every day.

serverless-esbuild addresses each of these.

What serverless-esbuild Does

It is a Serverless Framework plugin that wires esbuild into the deploy pipeline. The plugin:

  1. Detects which functions changed since the last deploy.
  2. Bundles each function and its dependencies with esbuild.
  3. Honors TypeScript, JSX, modern ECMAScript, path aliases and esbuild plugins.
  4. Minifies and tree-shakes by default.
  5. Uploads the artifact to your provider, usually AWS Lambda, through the Serverless Framework.

Because esbuild is written in Go and runs across cores, a Lambda function that takes 40 seconds to bundle with Webpack often bundles in under 200 milliseconds.

Getting Started

1. Install

npm install --save-dev serverless-esbuild esbuild typescript

If you use TypeScript:

npm install --save-dev @types/aws-lambda @types/node

2. Add the Plugin to serverless.yml

service: my-service

provider:
  name: aws
  runtime: nodejs18.x
  region: us-east-1
plugins:
  - serverless-esbuild
custom:
  esbuild:
    bundle: true
    minify: false
    sourcemap: true
    target: 'node18'
    exclude: ['aws-sdk']   # already provided by the Lambda runtime
functions:
  hello:
    handler: src/handlers/hello.handler

That is the whole setup. No separate webpack.config.js, no babel config, no tsconfig integration scripts.

3. Write Your Function

**src/handlers/hello.ts**

import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
import { greet } from '../utils/greet';

export const handler = async (
  event: APIGatewayProxyEvent
): Promise<APIGatewayProxyResult> => {
  const name = event.queryStringParameters?.name ?? 'world';
  return {
    statusCode: 200,
    body: JSON.stringify({ message: greet(name) }),
  };
};

**src/utils/greet.ts**

export const greet = (name: string): string => `Hello, ${name}!`;

4. Deploy

serverless deploy

Watch the logs. The “Packaging” step finishes quickly. Run it again and only the functions you changed get re-bundled.

Core Benefits

1. Build Speed

This is the main reason to use it. Numbers from a medium TypeScript project (about 40 functions, around 15k lines of code, with lodash, zod and the prisma client):

Bundler a) Cold bundle: Webpack 5 (38s) | esbuild (0.18s) b) Incremental bundle: Webpack 5 (9s) | esbuild (0.05s)

In CI this turns a 4-minute pipeline into a 45-second one. Locally, deploys feel responsive instead of blocking.

2. Smaller Bundles by Default

esbuild’s tree-shaking and minification do real work. A handler that ships at 480 KB with Webpack lands under 90 KB with esbuild. That gives you:

  • Faster cold starts on Lambda, since there is less to download and initialize.
  • Lower memory pressure during execution.
  • Lower cost at scale.

3. TypeScript, JSX and Modern JS Without Config

serverless-esbuild reads your tsconfig.json automatically. Path aliases, JSX, decorators and ESM all work. You no longer need:

// webpack.config.js
module.exports = {
  module: {
    rules: [
      { test: /\.tsx?$/, use: 'ts-loader' },
      // ...
    ],
  },
  resolve: {
    extensions: ['.ts', '.tsx', '.js'],
    alias: { '@': path.resolve(__dirname, 'src') },
  },
};

You configure esbuild instead and most of the time you do not need to configure even that.

4. Small Plugin

The plugin itself is small and has no dependencies. When you need to customize something, the options are documented in one README.

5. Incremental Builds

The plugin compares file hashes to find which functions changed. Only those get re-bundled. This is what makes it useful on monorepos and large services.

6. Plugin Support

Because it runs esbuild, you can use the esbuild plugin ecosystem. To inline environment variables, compile SASS, or inject build-time constants, register a plugin:

custom:
  esbuild:
    plugins: ./build/esbuild-plugins.js
// build/esbuild-plugins.js
const { define } = require('esbuild-plugin-defined');

module.exports = [define({ 'process.env.BUILD_TIME': JSON.stringify(new Date().toISOString()) })];

7. Watch Mode

Local development runs at the same speed:

serverless dev

Or invoke a function directly:

sls invoke local -f hello

The plugin caches aggressively, so later invocations skip the bundle step.

A Worked Example: TypeScript API with Shared Code

Here is a small API with two endpoints that share validation logic.

Project Structure

.
├── serverless.yml
├── tsconfig.json
├── package.json
└── src
    ├── handlers
    │   ├── createUser.ts
    │   └── getUser.ts
    ├── lib
    │   ├── db.ts
    │   └── validate.ts
    └── types
        └── user.ts

tsconfig.json

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "node",
    "esModuleInterop": true,
    "strict": true,
    "skipLibCheck": true,
    "resolveJsonModule": true,
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    }
  },
  "include": ["src/**/*"]
}

serverless.yml

service: user-api

provider:
  name: aws
  runtime: nodejs18.x
  region: us-east-1
plugins:
  - serverless-esbuild
custom:
  esbuild:
    bundle: true
    minify: true
    sourcemap: true
    target: 'node18'
    exclude: ['aws-sdk', 'pg-native']
    external: []
    plugins: ./build/esbuild-plugins.js
package:
  individually: true   # bundle each function separately
functions:
  createUser:
    handler: src/handlers/createUser.handler
    events:
      - httpApi:
          path: /users
          method: post
  getUser:
    handler: src/handlers/getUser.handler
    events:
      - httpApi:
          path: /users/{id}
          method: get

Shared Validation Logic

**src/lib/validate.ts**

import { z } from 'zod';

export const UserSchema = z.object({
  email: z.string().email(),
  name: z.string().min(1).max(100),
});
export type UserInput = z.infer<typeof UserSchema>;

The Create Handler

**src/handlers/createUser.ts**

import { APIGatewayProxyHandler } from 'aws-lambda';
import { UserSchema } from '../lib/validate';
import { db } from '../lib/db';

export const handler: APIGatewayProxyHandler = async (event) => {
  try {
    const body = JSON.parse(event.body ?? '{}');
    const user = UserSchema.parse(body);
    const created = await db.users.create(user);
    return { statusCode: 201, body: JSON.stringify(created) };
  } catch (err) {
    return {
      statusCode: 400,
      body: JSON.stringify({ error: (err as Error).message }),
    };
  }
};

The Get Handler

**src/handlers/getUser.ts**

import { APIGatewayProxyHandler } from 'aws-lambda';
import { db } from '../lib/db';

export const handler: APIGatewayProxyHandler = async (event) => {
  const id = event.pathParameters?.id;
  if (!id) return { statusCode: 400, body: 'Missing id' };

  const user = await db.users.findById(id);
  if (!user) return { statusCode: 404, body: 'Not found' };

  return { statusCode: 200, body: JSON.stringify(user) };
};

On deploy, each function gets its own bundle containing only the code it uses. Edit getUser.ts and only the getUser artifact is rebuilt and uploaded. A deploy that took 45 seconds drops to about 6.

Advanced Configuration

Externalizing Native Modules

Some Node modules such as sharp, prisma and pg-native ship native binaries. Keep them out of the bundle and add them to the deployment package:

custom:
  esbuild:
    exclude:
      - sharp
      - prisma
      - .prisma/client

package:
  patterns:
    - '!node_modules/.prisma/client/libquery_engine-*'
    - 'node_modules/.prisma/client/**'

Or use the native option for a shorter setup:

custom:
  esbuild:
    native: true

Per-Function Config Overrides

functions:
  largeWorker:
    handler: src/handlers/large.handler
    esbuild:
      minify: true
      target: 'node16'

Working with Serverless Layers

If you already package shared dependencies in a Lambda Layer, exclude them from each function’s bundle:

custom:
  esbuild:
    exclude:
      - '@aws-sdk/*'
      - 'lodash'

This cuts the per-function upload size.

Comparison

Featureserverless-webpackserverless-bundle(Parcel)serverless-esbuildBuild speedSlowMediumFastest of the threeTypeScript supportNeeds loader configBuilt-inBuilt-inJSX supportNeeds loader configBuilt-inBuilt-inPath aliasesNeeds configAuto from tsconfigAuto from tsconfigMinificationPlugin requiredBuilt-inBuilt-inTree-shakingPartialPartialAggressiveIncremental buildsYesLimitedYes, per functionPlugin ecosystemLargeLimitedesbuild pluginsCold start impactLarger bundleMedium bundleSmallest bundleMaturityVery matureMatureMature, still growing

For a new project, serverless-esbuild is a reasonable default. The maturity gap with the older plugins is small enough that it rarely matters for production workloads.

Common Gotchas

  1. Forgetting to exclude aws-sdk. Lambda Node.js 18 runtimes ship the v2 AWS SDK. Bundle your own copy and you bloat the function and can hit the 250 MB deployment limit on large projects. Set exclude: ['aws-sdk'].
  2. CommonJS-only libraries without an adapter. Some older packages call require() in ways esbuild cannot analyze statically. Add them to external when that happens.
  3. Path aliases live in tsconfig.json. The plugin reads tsconfig.json for path resolution, so check that baseUrl and paths are correct. On a JS-only project, set paths through the plugin options instead.
  4. Stale watch-mode caches. If local dev behaves oddly, clear the cache directory with rm -rf .esbuild.
  5. ESM-only dependencies. esbuild handles them, but the output still has to match your runtime. With runtime: nodejs18.x, keep CommonJS-style require in the bundled output, which is the default.

Practices Worth Adopting

  • Set package.individually: true for services with multiple functions to get the smallest per-function bundles.
  • Set sourcemap: true outside production. CloudWatch uses the maps to produce real stack traces.
  • Pin the esbuild version in package.json. Major esbuild releases sometimes change the plugin API.
  • Use exclude for anything the Lambda runtime or your Layers already provide.
  • Combine the plugin with package.patterns for control over what gets uploaded alongside the bundle.
  • Add a CI cache step for node_modules to speed up incremental deploys.

When Not to Use It

serverless-esbuild does not fit every case:

  • If you need Webpack-specific loaders for complex asset pipelines, esbuild’s plugin system is less flexible.
  • If you run an old Node.js runtime, esbuild needs a relatively modern Node version to execute, even when the output targets an older runtime.
  • If your team has standardized on a different bundler across many services, consistency can outweigh the speed gain.

For most greenfield serverless projects, though, it is the bundler to reach for.

Wrapping Up

The Serverless Framework’s strength has always been the infrastructure layer. serverless-esbuildbrings comparable speed to the application bundling layer:

  • Sub-second builds
  • Smaller Lambda artifacts
  • TypeScript, JSX and modern JS without config
  • The full esbuild plugin ecosystem
  • Per-function incremental deploys

It is a one-line change to your serverless.yml. Try it on your next service and watch where your CI time goes.

Resources


메타데이터
post_id
d47caae5f06e
slug
supercharge-your-serverless-deployments-with-serverless-esbuild-plugin-d47caae5f06e
url
https://medium.com/@manisuec/supercharge-your-serverless-deployments-with-serverless-esbuild-plugin-d47caae5f06e
canonical_url
https://medium.com/@manisuec/supercharge-your-serverless-deployments-with-serverless-esbuild-plugin-d47caae5f06e
author_url
https://medium.com/@manisuec
status
ok
fetched_at
2026-06-22 19:40:15