DEV Community

Cover image for I got tired of waiting for deploys, so I built a local Lambda runner
math
math

Posted on

I got tired of waiting for deploys, so I built a local Lambda runner

There's a specific kind of frustration that comes from iterating on Lambda code. You change one line. You run cdk deploy. You watch the CloudFormation stack crawl through its update. You wait. You test it. You realize the response shape was slightly off. You do it all over again.

Each cycle is 5 to 10 minutes. Over a full day of feature work, that's not a minor inconvenience. It's a significant chunk of your day spent watching a progress bar.

I started looking for a way out. I tried the obvious tools, found out why they fall short for CDK specifically, and eventually built something that actually solved the problem for me.


SAM CLI

AWS SAM is the official answer to this problem. sam local start-api spins up a local API Gateway + Lambda environment. On paper, it sounds perfect.

The catch? SAM requires you to maintain a template.yaml. If you're using CDK, that means maintaining two infrastructure definitions in parallel: your CDK stack and a SAM template. That felt like the wrong direction entirely. The whole point of CDK is that the infrastructure is code, colocated with my app. Introducing a second template that needs to stay in sync is exactly the kind of drift that leads to "works locally, breaks in prod."

There's no local hot reload. SAM has a sam sync --watch command, but it syncs your changes to real AWS, not to a local server. You still need a live AWS account and you're paying for each cycle. For the local sam local start-api server, if you ran sam build first, you have to re-run it every time you change code. There's no watching, no cache invalidation, nothing. It's a full rebuild to pick up any change.

LocalStack

LocalStack is impressive. It mocks basically the entire AWS API surface: S3, DynamoDB, SQS, Lambda, API Gateway, the works. If you need a full local AWS environment, it's probably the right tool.

But it comes with tradeoffs:

It's heavy. LocalStack runs as a Docker container with substantial memory overhead. Startup takes a while. For a fast iteration loop on a specific Lambda endpoint, it's overkill.

It still has drift risk. You're invoking your functions through a mock API Gateway. The invocation model is close but not identical to real AWS. Edge cases around authorizers, request context, and response formats can behave differently.

Hot reload is there, but awkward. LocalStack does support hot reloading for Lambda. You deploy to a magic S3 bucket called hot-reload pointing at your local directory, and it watches for changes. It works, but it's a LocalStack-specific setup you have to wire up manually, and it behaves differently from how your Lambda actually gets deployed on AWS.

The free tier is effectively gone. As of March 2026, LocalStack sunset its Community Edition. Core AWS services including Lambda are now behind a paid plan starting at $39/month. There is a free tier for non-commercial use, but if you're building something at work, you're paying. The community reaction was strong enough that open-source alternatives like MiniStack and floci started appearing within weeks.

For pure Lambda + API Gateway development, it's both overkill and increasingly expensive.


What I actually wanted

My CDK stack already describes everything: which Lambda function handles which route, what the authorizer is, what the method is. After cdk synth, all of that is sitting in a CloudFormation template in cdk.out/.

What I wanted was something that would read that template and just... run it locally. No second configuration file. No manual handler registry. No Docker.

So I built aws-cdk-local-lambda.

How it works

The package has two phases:

Extract reads your cdk.out/ CloudFormation template and produces a manifest describing all your routes, Lambda handlers, and authorizers. It recovers the TypeScript entry point for each handler from esbuild's bundle metadata, so you get back to your actual source files.

Serve takes that manifest and boots an Express server. Each API Gateway route becomes an Express route. When a request comes in, it bundles the TypeScript handler on demand with esbuild, invokes it with a proper APIGatewayProxyEvent, runs any custom authorizer configured for that route, and returns the response.

Hot reload is built in. When you save a file, the handler cache is invalidated and the next request re-bundles from source. No restart needed.

# one command to synth, extract, and serve with watch mode
npx cdk-local dev --cdk-out cdk.out --stack MyStack --stage dev --port 3001
Enter fullscreen mode Exit fullscreen mode

Or you can split it up:

# step 1: parse cdk.out into a manifest
npx cdk-local extract --cdk-out cdk.out --stack MyStack --stage dev --out .cdk-local/manifest.json

# step 2: serve from the manifest (hot reload on)
npx cdk-local serve --manifest .cdk-local/manifest.json --port 3001 --watch
Enter fullscreen mode Exit fullscreen mode

A typical package.json setup:

{
  "scripts": {
    "synth": "cdk synth --app 'tsx cdk/app.ts'",
    "extract": "cdk-local extract --cdk-out cdk.out --stack MyStack --stage dev --out .cdk-local/manifest.json",
    "manifest": "npm run synth && npm run extract",
    "serve": "cdk-local serve --manifest .cdk-local/manifest.json --port 3001 --watch",
    "dev": "npm run manifest && npm run serve"
  }
}
Enter fullscreen mode Exit fullscreen mode

Run npm run dev once when you start your session. After that, every code change you save is live within a second on the next request. No deploy cycle, no Docker, no second template to maintain.

The key difference from other approaches

Two things make this different from SAM or LocalStack.

The first is where it gets its source of truth. SAM reads a template you wrote. LocalStack mocks the AWS API. Both require you to keep something in sync with your actual CDK stack. This tool reads cdk synth output directly, so there is nothing to drift.

The second is how hot reload works. When you save a file, the server doesn't restart and it doesn't rebuild everything. It only invalidates the cache for the specific handler whose source file changed. The next request to that route re-bundles just that handler with esbuild and invokes the fresh code. Every other handler stays cached and responds instantly. You are never waiting on routes you didn't touch.


Limitations worth knowing

It's focused on API Gateway + Lambda. If your stack has SQS consumers, EventBridge rules, or Step Functions, those aren't handled. For that kind of full-stack local testing, LocalStack is probably still the right choice.

And it's a development tool, not a production server. Express is standing in for API Gateway; it's close but not identical. Don't use it for load testing or production routing logic validation.


Try it

npm install --save-dev aws-cdk-local-lambda
Enter fullscreen mode Exit fullscreen mode

The repo has a working simple-crud example with DynamoDB, custom authorizers, and the full route setup if you want to see it end to end.

If you're building on CDK and tired of the deploy-to-test loop, give it a shot, appreciate any feedback along the way.

Top comments (2)

Collapse
 
asish918 profile image
Asish

SAM CLI and Localstack are such a pain in the ass to work with for simple setups where we just want rapid iterations on Lambda code.

This tool was so needed. You have saved the dev community working on AWS Serverless Applications hours you might not even realise. Its really amazing!!

Collapse
 
merbayerp profile image
Mustafa ERBAY

What I find most interesting is that this isn’t really a Lambda story.

It’s a source-of-truth story.

SAM introduces another template.
LocalStack introduces another environment.
Both create opportunities for drift.

Reading directly from CDK synth output is an elegant idea because it removes an entire synchronization problem rather than trying to manage it better.

Some of the best engineering solutions don’t add abstractions.

They remove them.