In my previous post about using esbuild with Serverless, I shared a configuration that worked well at the time. However, after introducing a new dependency (snowflake-sdk), I had to revisit that setup — and along the way, I discovered that some of my earlier assumptions were incomplete or even wrong.
This post is a correction and clarification of that earlier article, based on what I learned during a much deeper debugging session.
The Trigger: Introducing snowflake-sdk
Recently, I needed to introduce a new dependency: snowflake-sdk.
Because snowflake-sdk includes native .node binaries, I followed the common advice and added it to the external field in my esbuild configuration:
external:
- snowflake-sdk
The intention was simple:
Let esbuild skip bundling Snowflake, and load it at runtime instead.
Unfortunately, this time the build failed.
During compilation, esbuild encountered .node files and threw an error. This was my first signal that something fundamental had changed compared to my previous setup.
A Temporary Fix That Created a Bigger Problem
To unblock myself, I tried something that appeared to work:
I moved my esbuild configuration from custom.esbuild
Back to top-level esbuild
Surprisingly, the build passed.
However, this introduced a much more serious issue.
My previously working package.patterns configuration — which carefully excluded unnecessary Prisma files — stopped working entirely.
As a result:
A large number of unnecessary @prisma/client runtime files were bundled
The final Lambda artifact grew far beyond AWS Lambda’s size limit
-
I was stuck in a dilemma:
- Keep Snowflake working but exceed Lambda limits
- Control bundle size but break Snowflake
Clearly, I didn’t fully understand what was happening yet.
The Key Question: Why Does esbuild Behave Differently at Top Level?
At this point, the real question became:
Why does package.patterns work when esbuild is defined at the top level, but not when it lives under custom.esbuild?
After a lot of trial, error, and reading through build outputs, I realized the answer was not about package.patterns at all.
The real issue was external.
The Real Root Cause: external Changes the Packaging Model
Here is the critical insight:
When a dependency (for example @prisma/client) is listed in external
Serverless assumes it must be available at runtime
To guarantee this, it stops respecting package.patterns exclusions for that dependency
Instead, it ensures the dependency (and its transitive files) are included in the final artifact
In other words:
external implicitly overrides your intention to exclude files.
This explains everything.
Why My Previous Setup “Worked by Accident”
In my earlier build:
esbuild was configured at the top level
@prisma/client was listed in external
What I didn’t realize at the time was that:
The top-level esbuild path caused Serverless to use a different internal build flow
In that flow, external did not behave the same way
As a side effect, Prisma files were not force-included, and my bundle stayed small
So my previous success was not due to a correct understanding — it was an accidental alignment of behaviors.
The Correct Fix: Remove Prisma from external
Once I understood this, the correct solution became clear.
I tried one final experiment:
- Keep esbuild under custom.esbuild
- Remove @prisma/client from the external list
- Let esbuild bundle Prisma normally
- Keep package.patterns in place
To my surprise — and relief — it worked.
- The build succeeded
- Snowflake was handled separately
- Prisma files were properly tree-shaken
- The final bundle size stayed under 50 MB
- No Lambda size limits were violated
This time, it worked for the right reasons.
Final Takeaways
Here are the key lessons I took away from this experience:
external is not just a bundling hint
It fundamentally changes how Serverless treats dependencies during packaging.package.patterns cannot override runtime guarantees
If a dependency is marked as external, Serverless will ensure it exists — even if that means ignoring exclusions.Top-level esbuild vs custom.esbuild can change behavior
Not because one is “better”, but because they trigger different internal build paths.A working build does not always mean a correct build
My earlier configuration worked by coincidence, not by design.
Conclusion
This experience reminded me that build systems often fail silently — until they don’t. What looked like a simple Snowflake integration turned into a deep dive into how esbuild, Serverless, and packaging rules interact.
Hopefully, this correction helps others avoid the same confusion — and encourages a bit of healthy skepticism when a configuration “just works” without a clear explanation.
If you’re using esbuild with Serverless, especially with Prisma or native dependencies, understanding how external truly behaves is essential.
Top comments (0)