Cold start optimization for serverless functions

Our company is engaged in the development, support and maintenance of sites of any complexity. From simple one-page sites to large-scale cluster systems built on micro services. Experience of developers is confirmed by certificates from vendors.
Development and maintenance of all types of websites:
Informational websites or web applications
Business card websites, landing pages, corporate websites, online catalogs, quizzes, promo websites, blogs, news resources, informational portals, forums, aggregators
E-commerce websites or web applications
Online stores, B2B portals, marketplaces, online exchanges, cashback websites, exchanges, dropshipping platforms, product parsers
Business process management web applications
CRM systems, ERP systems, corporate portals, production management systems, information parsers
Electronic service websites or web applications
Classified ads platforms, online schools, online cinemas, website builders, portals for electronic services, video hosting platforms, thematic portals

These are just some of the technical types of websites we work with, and each of them can have its own specific features and functionality, as well as be customized to meet the specific needs and goals of the client.

Our competencies:
Development stages
Latest works
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1041
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    822
  • image_crm_chasseurs_493_0.webp
    CRM development for Chasseurs
    847
  • image_website-sbh_0.png
    Website development for SBH Partners
    999
  • image_website-_0.png
    Website development for Red Pear
    451

Serverless Function Cold Start Optimization

Cold start—delay on first Lambda invoke after idle period or scaling to new instance. Consists of multiple phases: container creation (~100–500ms), runtime init (~50–200ms for Node.js), your function init code (depends on you). First two phases barely optimizable—all you can do is work with third.

Real numbers for Node.js 20 on AWS Lambda: without optimizations init-phase takes 300–800ms. After optimizations—50–150ms. On arm64 (Graviton2) runtime-phase 10–20% faster x86.

What Happens at Cold Start

Cold start phases:
1. Container init    │ ~100-500ms  │ AWS manages, not optimizable
2. Runtime init      │ ~50-200ms   │ depends on runtime and architecture
3. Function init     │ YOUR CODE   │ require/import, DB connections, SDK init
4. Handler execution │ YOUR CODE   │ actual function work

Profile init-phase via environment variable:

# Add to Lambda environment
AWS_LAMBDA_EXEC_WRAPPER=/opt/aws-lambda-exec-wrapper
# or use built-in profiling

Best way to measure—CloudWatch Logs. Look for Init Duration line:

REPORT RequestId: abc123
Duration: 45.23 ms
Billed Duration: 46 ms
Memory Size: 512 MB
Max Memory Used: 89 MB
Init Duration: 312.45 ms  ← this is cold start overhead

Reducing Bundle Size

Main reason for slow cold start—large bundle with unnecessary modules.

Before optimization:

// Bad—imports entire AWS SDK
import AWS from 'aws-sdk';
const s3 = new AWS.S3();
const dynamo = new AWS.DynamoDB.DocumentClient();

After:

// Good—only needed clients, AWS SDK v3 modular
import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient, GetCommand } from '@aws-sdk/lib-dynamodb';

// Initialize once outside handler
const s3 = new S3Client({ region: process.env.AWS_REGION });
const dynamo = DynamoDBDocumentClient.from(new DynamoDBClient({}));

Exclude AWS SDK from bundle (already in Lambda runtime for Node.js 18+):

// esbuild config
{
  "external": ["@aws-sdk/*"],
  "bundle": true,
  "minify": true,
  "target": "node20",
  "platform": "node"
}

Sizes before/after. Typical Express app with aws-sdk v2:

  • Before: 8–15 MB zip
  • After: 500KB–2MB zip

Lazy Loading for Rarely Used Modules

// Bad—everything loads on init
import { createCanvas } from 'canvas';
import sharp from 'sharp';
import { PDFDocument } from 'pdf-lib';

export const handler = async (event) => {
  if (event.type === 'generate-pdf') {
    // used 5% of invocations
    const pdf = await PDFDocument.create();
  }
};

// Good—heavy modules only when needed
export const handler = async (event) => {
  if (event.type === 'generate-pdf') {
    const { PDFDocument } = await import('pdf-lib');
    const pdf = await PDFDocument.create();
  }
};

Initialization Outside Handler

Code outside handler executes once at cold start and reused between warm invocations:

import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';

// INIT PHASE—once
const client = new DynamoDBClient({
  // Reuse TCP connections
  requestHandler: {
    requestTimeout: 3000,
    httpsAgent: { keepAlive: true, maxSockets: 50 },
  },
});
const dynamo = DynamoDBDocumentClient.from(client);

// Read environment variables once
const TABLE_NAME = process.env.TABLE_NAME!;
const STAGE = process.env.STAGE ?? 'dev';

// HANDLER—executes every time
export const handler = async (event) => {
  // dynamo already initialized, connection reused
  const result = await dynamo.send(new GetCommand({
    TableName: TABLE_NAME,
    Key: { pk: event.userId, sk: 'profile' },
  }));

  return result.Item;
};

Database Connection Optimization

Standard connection pool (pg Pool, Sequelize) doesn't work in serverless—each Lambda instance creates its own connection, at 1000 concurrent executions you get 1000 connections to PostgreSQL.

Solution 1: RDS Proxy (AWS-managed connection pooler):

// Connect to RDS Proxy, not directly to RDS
import { Pool } from 'pg';

const pool = new Pool({
  host: process.env.RDS_PROXY_ENDPOINT,  // xxx.proxy-xxx.rds.amazonaws.com
  database: process.env.DB_NAME,
  user: process.env.DB_USER,
  password: process.env.DB_PASSWORD,
  max: 1,          // 1 connection per Lambda instance
  idleTimeoutMillis: 0,
  connectionTimeoutMillis: 5000,
});

// Reuse between warm invocations
let cachedClient: pg.PoolClient | null = null;

export const getDbClient = async () => {
  if (!cachedClient) {
    cachedClient = await pool.connect();
  }
  return cachedClient;
};

Solution 2: PlanetScale or Neon—HTTP-based serverless databases without persistent connections:

import { neon } from '@neondatabase/serverless';

// Each request—HTTP, not TCP
const sql = neon(process.env.DATABASE_URL!);

export const handler = async (event) => {
  const users = await sql`SELECT id, email FROM users WHERE active = true LIMIT 10`;
  return users;
};

Provisioned Concurrency

Provisioned Concurrency—AWS keeps N Lambda instances always warmed. Init-phase executes beforehand, user gets response without delay.

# serverless.yml or SAM template
Resources:
  ApiFunction:
    Type: AWS::Serverless::Function
    Properties:
      # ...
      AutoPublishAlias: live

  ApiProvisionedConcurrency:
    Type: AWS::Lambda::ProvisionedConcurrencyConfig
    Properties:
      FunctionName: !Ref ApiFunction
      Qualifier: live
      ProvisionedConcurrentExecutions: 5  # keep 5 warm instances

Via Serverless Framework:

functions:
  api:
    handler: src/handler.main
    provisionedConcurrency: 5
    # Auto-scaling via Application Auto Scaling
    # configured separately via AWS Console or CDK

Provisioned Concurrency costs (billed even when idle), so use only for critical endpoints.

arm64 vs x86_64

Switch to Graviton2 (arm64)—simplest optimization with zero code changes:

# SAM template
Globals:
  Function:
    Architectures: [arm64]  # was x86_64

Benefit: ~10–20% less init duration, ~20% cheaper on AWS rates. Only limitation: native Node.js modules (.node files) need recompiling for arm64. Regular JS/TS modules work unchanged.

Timeline

Bundle audit and basic optimization (esbuild, tree shaking, AWS SDK extraction)—1 day. Rework initialization with client extraction from handler, RDS Proxy setup—2–3 days. Provisioned Concurrency setup with monitoring and auto-scaling—1–2 days. Full cycle: from audit to production with measurable result—1 week.