For my real estate marketplace project, Kiray, my rule is strict type safety. The backend uses a NestJS and TypeScript architecture with explicit types everywhere. But when I started building the background image processing using AWS Lambda, my stack strategy hit a major real-world hurdle.
To handle high-speed image resizing and compression, I chose sharp. Sharp is fast because it uses a native C++ library called libvips under the hood rather than pure JavaScript.
However, because it uses native C++ code, it compiles itself specifically for the operating system where you run npm install. When I installed it on my Mac, npm downloaded the macOS binary. But when that code ran on AWS Lambda (which runs Amazon Linux 2), the function immediately crashed with a dynamic linking mismatch error:
Error: /opt/nodejs/node_modules/sharp/build/Release/sharp-linux-x64.node: invalid ELF header
The Lambda container encountered a macOS binary on a Linux system. To fix this compilation mismatch, I had to force npm to target the production environment explicitly during development:
npm install sharp --platform=linux --arch=x64 --libc=glibc
Once I had the correct Linux binary, I had to decide how to deploy it. My first instinct was to zip up the node_modules folder with each individual Lambda function. The math quickly showed why this is a poor approach:
- Sharp's Linux binary bundle size: ~8.4MB
- Independent functions needing Sharp: 2 (
generate-thumbnailandgenerate-medium) - Total impact: 16.8MB of duplicate files traveling through the deployment pipeline.
This duplication slows down deployment speeds and inflates function package sizes unnecessarily. To solve this, I decoupled the heavy dependency using an AWS Lambda Layer.
Layer Strategy:
└── kiray-sharp-layer (8.4MB Payload — Deployed Once)
└── nodejs/node_modules/sharp/ (Linux x64 Binary)
├── generate-thumbnail/index.js (< 10KB Zip — References Layer)
└── generate-medium/index.js (< 10KB Zip — References Layer)
By uploading Sharp once to a shared layer, the individual zip files for my functions dropped from megabytes down to under 10KB.
While the rest of Kiray is strict TypeScript, I consciously chose to write these specific Lambda handlers in pure JavaScript for three reasons:
1. Build Step Overhead: AWS Lambda does not run TypeScript natively. Adding a compilation toolchain (like tsc or a bundler) for isolated utility functions that are only 50 lines long adds unnecessary complexity to the CI/CD pipeline.
2. Simpler Debugging: Troubleshooting native C++ binaries in the cloud is already complicated. Adding an abstract code compilation layer on top makes it harder to isolate low-level bugs.
3. Diminishing Returns: These image utility handlers are small, single-purpose, and functionally stable. The benefit of static type checking here is low compared to the maintenance effort of managing the extra build tools.
What this reinforced:Local success doesn't guarantee cloud success. When dealing with native modules, what works flawlessly on your laptop can easily fail in production if the underlying operating systems don't match. Knowing when to choose simplicity over a strict architectural principle is part of building real, high-velocity systems.




