Files
MinglarBackendNestJS/LAMBDA_OPTIMIZATION_GUIDE.md

13 KiB

AWS Lambda Bundle Size Optimization Guide

Overview

This guide documents how to optimize AWS Lambda function bundle sizes when using:

  • Serverless Framework v4 (with built-in esbuild)
  • Prisma ORM (with driver adapters)
  • NestJS or any Node.js framework

Problem

Lambda functions can become bloated (25+ MB) due to:

  1. Prisma binary engines (~50MB uncompressed)
  2. AWS SDK v3 being bundled (~5-10MB)
  3. Dependencies copied to node_modules instead of being bundled

Solution Architecture

┌─────────────────────────────────────────────────────────┐
│                    Lambda Function                       │
│  ┌─────────────────────────────────────────────────┐    │
│  │  Your Code (bundled by esbuild)     ~50-500 KB  │    │
│  └─────────────────────────────────────────────────┘    │
│                         │                                │
│                         ▼                                │
│  ┌─────────────────────────────────────────────────┐    │
│  │  Prisma Layer (shared)              ~15 MB      │    │
│  │  - @prisma/client                               │    │
│  │  - @prisma/adapter-pg                           │    │
│  │  - .prisma/client (generated)                   │    │
│  │  - pg driver                                    │    │
│  └─────────────────────────────────────────────────┘    │
│                         │                                │
│                         ▼                                │
│  ┌─────────────────────────────────────────────────┐    │
│  │  AWS Lambda Runtime                             │    │
│  │  - AWS SDK v3 (built-in for Node.js 18+)        │    │
│  └─────────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────────┘

Step-by-Step Setup

1. Project Structure

your-project/
├── serverless.yml
├── package.json
├── prisma/
│   └── schema.prisma
├── layers/
│   └── prisma/
│       └── nodejs/
│           └── package.json
├── src/
│   └── ... your code
└── build-prisma-layer.ps1  (or .sh for Linux/Mac)

2. Prisma Schema Configuration

prisma/schema.prisma

generator client {
  provider        = "prisma-client-js"
  // For Prisma 7+ with driver adapters, no binary targets needed
  // The WASM-based query engine is used automatically
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

Note

: Prisma 7+ uses WASM-based query compiler instead of binary engines when using driver adapters, which is much smaller.

3. Layer Package.json

layers/prisma/nodejs/package.json

{
  "name": "prisma-layer",
  "version": "1.0.0",
  "description": "Lambda layer for Prisma with pg driver adapter",
  "dependencies": {
    "@prisma/client": "^7.0.0",
    "@prisma/adapter-pg": "^7.0.0",
    "pg": "^8.13.0"
  }
}

4. Serverless Configuration

serverless.yml

service: your-service-name

provider:
  name: aws
  runtime: nodejs22.x
  region: your-region
  memorySize: 512
  # Apply Prisma layer to ALL functions
  layers:
    - !Ref PrismaLambdaLayer
  environment:
    DATABASE_URL: ${env:DATABASE_URL}
    # ... other env vars

# esbuild configuration (Serverless v4 built-in)
build:
  esbuild:
    bundle: true
    minify: true
    sourcemap: false
    target: node20
    platform: node
    # Mark packages as external (not bundled into JS)
    external:
      - '@prisma/client'
      - '@prisma/adapter-pg'
      - '.prisma/client'
      - '.prisma'
      - 'pg'
      - '@aws-sdk/*'
      - '@smithy/*'
      - '@aws-crypto/*'
    # Exclude from npm install in zip (CRITICAL!)
    exclude:
      - 'aws-sdk'
      - '@aws-sdk/*'
      - '@smithy/*'
      - '@aws-crypto/*'
      - '@prisma/client'
      - '@prisma/adapter-pg'
      - '.prisma'
      - '.prisma/client'
      - 'pg'
      - 'pg-*'
      - 'postgres-*'
      - 'pgpass'
      - 'split2'
      - 'xtend'

# Define the Prisma layer
layers:
  prisma:
    path: layers/prisma
    name: ${self:service}-prisma-layer-${sls:stage}
    description: Prisma client with pg driver adapter
    compatibleRuntimes:
      - nodejs22.x
    retain: false

# Package configuration
package:
  individually: true
  patterns:
    - '!node_modules/**'
    - '!node_modules/@prisma/**'
    - '!node_modules/.prisma/**'
    - '!**/*.test.js'
    - '!**/*.spec.js'
    - '!**/test/**'
    - '!**/__tests__/**'
    - '!package-lock.json'
    - '!yarn.lock'
    - '!README.md'
    - '!.git/**'

functions:
  myFunction:
    handler: src/handlers/myHandler.handler
    events:
      - httpApi:
          path: /my-endpoint
          method: get

plugins:
  - serverless-offline

5. Build Script for Prisma Layer

Windows (PowerShell) - build-prisma-layer.ps1

# Build Prisma Lambda Layer
$layerPath = "layers\prisma\nodejs"

Write-Host "Building Prisma Lambda Layer..." -ForegroundColor Cyan

# 1. Clean existing node_modules in layer
Write-Host "Cleaning layer node_modules..."
if (Test-Path "$layerPath\node_modules") {
    Remove-Item -Recurse -Force "$layerPath\node_modules"
}

# 2. Install dependencies in layer
Write-Host "Installing layer dependencies..."
Push-Location $layerPath
npm install --omit=dev
Pop-Location

# 3. Generate Prisma client
Write-Host "Generating Prisma client..."
npx prisma generate

# 4. Copy .prisma/client to layer
Write-Host "Copying generated Prisma client to layer..."
$sourcePrisma = "node_modules\.prisma"
$destPrisma = "$layerPath\node_modules\.prisma"

if (Test-Path $sourcePrisma) {
    if (Test-Path $destPrisma) {
        Remove-Item -Recurse -Force $destPrisma
    }
    Copy-Item -Recurse $sourcePrisma $destPrisma
    Write-Host "Copied .prisma/client successfully!" -ForegroundColor Green
} else {
    Write-Host "ERROR: .prisma folder not found. Run 'npx prisma generate' first." -ForegroundColor Red
    exit 1
}

# 5. Show layer size
$layerSize = (Get-ChildItem "$layerPath\node_modules" -Recurse | Measure-Object -Property Length -Sum).Sum / 1MB
Write-Host "`nTotal layer size: $([math]::Round($layerSize, 2)) MB" -ForegroundColor Yellow
Write-Host "Prisma layer built successfully!" -ForegroundColor Green

Linux/Mac (Bash) - build-prisma-layer.sh

#!/bin/bash
set -e

LAYER_PATH="layers/prisma/nodejs"

echo "Building Prisma Lambda Layer..."

# 1. Clean existing node_modules in layer
echo "Cleaning layer node_modules..."
rm -rf "$LAYER_PATH/node_modules"

# 2. Install dependencies in layer
echo "Installing layer dependencies..."
cd "$LAYER_PATH"
npm install --omit=dev
cd -

# 3. Generate Prisma client
echo "Generating Prisma client..."
npx prisma generate

# 4. Copy .prisma/client to layer
echo "Copying generated Prisma client to layer..."
if [ -d "node_modules/.prisma" ]; then
    rm -rf "$LAYER_PATH/node_modules/.prisma"
    cp -r "node_modules/.prisma" "$LAYER_PATH/node_modules/.prisma"
    echo "Copied .prisma/client successfully!"
else
    echo "ERROR: .prisma folder not found. Run 'npx prisma generate' first."
    exit 1
fi

# 5. Show layer size
LAYER_SIZE=$(du -sm "$LAYER_PATH/node_modules" | cut -f1)
echo "Total layer size: ${LAYER_SIZE} MB"
echo "Prisma layer built successfully!"

6. Prisma Client Usage

src/common/database/prisma.client.ts

import { PrismaClient } from '@prisma/client';
import { PrismaPg } from '@prisma/adapter-pg';
import { Pool } from 'pg';

// Connection pool for serverless
const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
  max: 5, // Limit connections in Lambda
});

const adapter = new PrismaPg(pool);

// Single instance for Lambda warm starts
let prisma: PrismaClient;

export function getPrismaClient(): PrismaClient {
  if (!prisma) {
    prisma = new PrismaClient({
      adapter,
      log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
    });
  }
  return prisma;
}

export { prisma };

Deployment Workflow

# 1. Build the Prisma layer (run after schema changes)
./build-prisma-layer.ps1   # Windows
# or
./build-prisma-layer.sh    # Linux/Mac

# 2. Deploy
npx serverless deploy --stage=dev

# 3. Deploy single function (faster, uses existing layer)
npx serverless deploy function -f myFunction --stage=dev

Key Configuration Explained

esbuild external vs exclude

Property Purpose
external Tells esbuild NOT to bundle these into the JS file. They become require() calls.
exclude Tells Serverless NOT to npm install these packages into the function zip.

Both are required!

  • external alone = esbuild doesn't bundle, but Serverless still installs to node_modules
  • exclude alone = Serverless doesn't install, but esbuild bundles the code

Layer Reference

# This creates a CloudFormation reference to the layer defined in the same stack
layers:
  - !Ref PrismaLambdaLayer

The PrismaLambdaLayer name comes from the layer key (prisma) converted to PascalCase + LambdaLayer.

Why Exclude pg-* packages?

When pg is external, its dependencies still get installed:

  • pg-connection-string
  • pg-pool
  • pg-protocol
  • pg-types
  • etc.

These must all be in the exclude list to prevent duplication.


Expected Results

Function Type Before After
Simple handlers 25+ MB 50-100 kB
With validation (zod/yup) 25+ MB 300-500 kB
With S3 uploads 30+ MB 1-2 MB

Troubleshooting

1. "Cannot find module '@prisma/client'"

Cause: Layer doesn't have the generated .prisma/client

Fix: Run build-prisma-layer.ps1 to regenerate the layer

2. Function size still large

Debug: Extract and inspect the zip:

Expand-Archive ".serverless\build\your-function.zip" -DestinationPath "extracted"
Get-ChildItem "extracted\node_modules" -Directory

If you see @prisma or pg folders, the exclude config isn't working.

3. "Cannot resolve CloudFormation reference"

Cause: Using ${cf:...} reference before first deploy

Fix: Use !Ref PrismaLambdaLayer instead (works on first deploy)

4. Cold starts still slow

Consider:

  • Provisioned Concurrency: Pre-warm instances
  • Reduce memory: Sometimes lower memory = same speed, lower cost
  • Connection pooling: Use tools like PgBouncer for RDS

Additional Optimizations

1. Remove duplicate validation libraries

Pick ONE of: zod, yup, or class-validator. Don't use all three.

2. Tree-shake NestJS

If not using full NestJS, import only what you need:

// Instead of
import { Controller, Get } from '@nestjs/common';

// For Lambda handlers, you might not need NestJS at all

3. Use AWS SDK v3 selectively

external:
  - '@aws-sdk/*'  # Exclude all

Then in code:

// AWS SDK v3 is available in Lambda runtime (Node.js 18+)
import { S3Client } from '@aws-sdk/client-s3';

Quick Reference

# Minimal serverless.yml for Prisma + Lambda optimization
build:
  esbuild:
    bundle: true
    minify: true
    external:
      - '@prisma/client'
      - '@prisma/adapter-pg'
      - '.prisma/client'
      - '.prisma'
      - 'pg'
      - '@aws-sdk/*'
    exclude:
      - '@prisma/client'
      - '@prisma/adapter-pg'
      - '.prisma'
      - '.prisma/client'
      - 'pg'
      - 'pg-*'
      - 'postgres-*'
      - 'pgpass'
      - 'split2'
      - 'xtend'
      - '@aws-sdk/*'

layers:
  prisma:
    path: layers/prisma
    name: ${self:service}-prisma-${sls:stage}
    compatibleRuntimes:
      - nodejs22.x

provider:
  layers:
    - !Ref PrismaLambdaLayer

Version Compatibility

Tool Tested Version
Serverless Framework v4.x
Prisma v7.x
Node.js 20.x, 22.x
AWS Lambda Runtime nodejs20.x, nodejs22.x

Last updated: December 2025