← Back to ArticlesFunctions

Azure Functions — Cold Start Prevention and Performance Optimization

Practical strategies to minimize cold starts, improve response times, and optimize Azure Functions for production workloads.

Azure Functions — Cold Start Prevention and Performance Optimization

The Problem

Your Azure Functions experience:

You need practical solutions to minimize cold starts and optimize performance.

Understanding Cold Starts

┌─────────────────────────────────────────────────────────────────┐
│                    Function Execution Timeline                  │
└─────────────────────────────────────────────────────────────────┘

Cold Start (5-15 seconds):
┌───────────┬───────────┬────────┬────────┬────────┬──────────┬───────────┐
│ Instance  │ Loading   │ Runtime│  Init  │   JIT  │ Business │ Response  │
│ Selection │ Docker    │  Start │  Code  │ Compile│  Logic   │           │
└───────────┴───────────┴────────┴────────┴────────┴──────────┴───────────┘
 2-4 sec   1-2 sec  1-2 sec  1-3 sec  1-2 sec  Variable   Done

Warm Start (50-200ms):
┌────────┬─────────────┐
│  JIT   │ Business    │
│ Compile│ Logic       │
└────────┴─────────────┘

Always Running (<50ms):
┌─────────────┐
│  Business   │
│  Logic      │
└─────────────┘

Solution Implementation

Step 1: Keep Functions Warm with Proactive Pings

// warmup/index.ts - Timer trigger to keep functions warm
import { AzureFunction, Context, Timer } from "@azure/functions";

const warmupTrigger: AzureFunction = async function (context: Context, timer: Timer): Promise<void> {
    if (timer.isPast) {
        context.log("Warmup: Timer triggered");
        
        // List of functions to warm up
        const functionsToWarm = [
            "https://yourapp.azurewebsites.net/api/orders",
            "https://yourapp.azurewebsites.net/api/customers",
            "https://yourapp.azurewebsites.net/api/products"
        ];
        
        // Ping each function with a light request
        await Promise.allSettled(
            functionsToWarm.map(async (url) => {
                try {
                    const response = await fetch(url, {
                        method: "GET",
                        headers: { "X-Warmup": "true" },
                        signal: AbortSignal.timeout(10000)
                    });
                    context.log(`Warmed up: ${url}, status: ${response.status}`);
                } catch (error) {
                    context.log(`Warmup failed for ${url}: ${error}`);
                }
            })
        );
    }
};

export default warmupTrigger;
// function.json for warmup
{
  "bindings": [
    {
      "name": "timer",
      "type": "timerTrigger",
      "direction": "in",
      "schedule": "*/5 * * * *", // Every 5 minutes
      "runOnStartup": true
    }
  ]
}

Step 2: Lazy Initialization Pattern

// Lazy service initialization
import { AzureFunction, Context, HttpRequest } from "@azure/functions";

interface Services {
    db: DatabaseService;
    cache: CacheService;
    queue: QueueService;
}

// Lazy initialization container
class ServiceContainer {
    private _db?: DatabaseService;
    private _cache?: CacheService;
    private _queue?: QueueService;
    
    get db(): DatabaseService {
        if (!this._db) {
            this._db = new DatabaseService(process.env["DATABASE_CONNECTION"]!);
        }
        return this._db;
    }
    
    get cache(): CacheService {
        if (!this._cache) {
            this._cache = new CacheService(process.env["REDIS_CONNECTION"]!);
        }
        return this._cache;
    }
    
    get queue(): QueueService {
        if (!this._queue) {
            this._queue = new QueueService(process.env["QUEUE_CONNECTION"]!);
        }
        return this._queue;
    }
}

// Singleton for warm instances
const services = new ServiceContainer();

const httpTrigger: AzureFunction = async function (context: Context, req: HttpRequest): Promise<void> {
    // Only initialize what we need
    const useCache = req.query.get("cache") === "true";
    
    if (useCache) {
        // Cache is lazily initialized only when needed
        const cached = await services.cache.get(req.query.get("id")!);
        if (cached) {
            context.res = { json: cached };
            return;
        }
    }
    
    const data = await services.db.query(req.query.get("id")!);
    
    if (useCache) {
        await services.cache.set(req.query.get("id")!, data, 300);
    }
    
    context.res = { json: data };
};

export default httpTrigger;

Step 3: Connection Pooling and Reuse

// http-trigger.ts - Reuse HTTP client
import { AzureFunction, Context, HttpRequest } from "@azure/functions";

// Create client once, reuse across invocations
const httpClient = new HttpClient();

const httpTrigger: AzureFunction = async function (context: Context, req: HttpRequest): Promise<void> {
    // Reuse the client - no new connection per request
    const response = await httpClient.post(
        "https://api.example.com/data",
        JSON.stringify(req.body),
        {
            headers: { "Content-Type": "application/json" }
        }
    );
    
    context.res = {
        status: response.status,
        json: await response.json()
    };
};
// database-trigger.ts - Use connection pool
import { Pool } from "pg";

// Module-level connection pool (persists across warm invocations)
let pool: Pool | null = null;

function getPool(): Pool {
    if (!pool) {
        pool = new Pool({
            connectionString: process.env["DATABASE_CONNECTION"],
            max: 20,              // Max connections
            idleTimeoutMillis: 30000,
            connectionTimeoutMillis: 5000
        });
    }
    return pool;
}

export const handler: AzureFunction = async function (context: Context, req: HttpRequest): Promise<void> {
    const client = await getPool().connect();
    
    try {
        const result = await client.query("SELECT * FROM orders WHERE id = $1", [req.params.id]);
        context.res = { json: result.rows };
    } finally {
        client.release(); // Return to pool, don't close
    }
};

Step 4: Node.js Optimizations

// host.json optimizations
{
  "version": "2.0",
  "extensionBundle": {
    "id": "Microsoft.Azure.Functions.ExtensionBundle",
    "version": "[4.*, 5.0.0)"
  },
  "functionTimeout": "00:05:00",
  "retry": {
    "strategy": "exponentialBackoff",
    "maxRetryCount": 5,
    "delayInterval": "00:00:05"
  },
  "healthMonitor": {
    "enabled": true,
    "healthCheckInterval": "00:00:10",
    "healthCheckThreshold": 6
  },
  "logging": {
    "logLevel": {
      "default": "Information",
      "Host.Results": "Warning",
      "Function": "Warning"
    }
  },
  "extensions": {
    "http": {
      "routePrefix": "api",
      "maxConcurrentRequests": 100,
      "maxOutstandingRequests": 200
    }
  }
}
// webpack.config.js - Bundle optimization
module.exports = {
  entry: "./src/index.ts",
  output: {
    libraryTarget: "commonjs2",
    path: __dirname + "/dist"
  },
  resolve: {
    extensions: [".ts", ".js"]
  },
  module: {
    rules: [
      {
        test: /\.ts$/,
        use: "ts-loader",
        exclude: /node_modules/
      }
    ]
  },
  optimization: {
    minimize: true,
    sideEffects: false
  },
  // Prebundle dependencies
  externals: {
    "pg": "pg",
    "redis": "redis"
  }
};

Step 5: Premium Plan with Always Ready Instances

# Create Premium plan with always ready instances
az functionapp plan create \
  --name my-function-plan \
  --resource-group my-rg \
  --location eastus \
  --sku EP2 \
  --min-instances 2 \
  --maximum-worker-count 10

# Create function app with the plan
az functionapp create \
  --name my-function-app \
  --resource-group my-rg \
  --plan my-function-plan \
  --runtime node \
  --runtime-version 20 \
  --storage-account mystorageaccount

# Configure always-on
az functionapp config set \
  --name my-function-app \
  --resource-group my-rg \
  --always-on true

# Set pre-warmed instances
az functionapp update \
  --name my-function-app \
  --resource-group my-rg \
  --minimum-elastic-instance-count 2

Step 6: Memory Optimization

// Optimize memory usage
export const handler: AzureFunction = async function (context: Context, req: HttpRequest): Promise<void> {
    // 1. Stream responses instead of buffering
    const stream = await fetchLargeData();
    
    context.res = {
        body: stream,
        headers: {
            "Content-Type": "application/json",
            "Transfer-Encoding": "chunked"
        }
    };
    
    // 2. Clear large objects when done
    stream.close();
};

// Chunk processing to avoid memory spikes
async function processLargeDataset(): Promise<void> {
    const BATCH_SIZE = 1000;
    let offset = 0;
    
    while (true) {
        // Process in batches, don't load all into memory
        const batch = await fetchBatch(offset, BATCH_SIZE);
        
        if (batch.length === 0) break;
        
        await processBatch(batch);
        
        // Force garbage collection hint (if supported)
        if (global.gc) global.gc();
        
        offset += BATCH_SIZE;
    }
}

Step 7: HTTP Trigger Optimization

// Optimize HTTP response
const optimizedHandler: AzureFunction = async (context: HttpRequest): Promise<void> => {
    // Use simple objects, avoid deep cloning
    const data = getData(); // Already structured efficiently
    
    context.res = {
        status: 200,
        // Send directly, don't transform unnecessarily
        body: data
    };
};

// Cache expensive computations
const computeCache = new Map<string, { data: any; expires: number }>();

const cachedHandler: AzureFunction = async (context: HttpRequest): Promise<void> => {
    const cacheKey = `${context.req.method}:${context.req.url}`;
    
    const cached = computeCache.get(cacheKey);
    if (cached && cached.expires > Date.now()) {
        context.res = { json: cached.data, headers: { "X-Cache": "HIT" } };
        return;
    }
    
    const data = await expensiveCompute();
    
    computeCache.set(cacheKey, { data, expires: Date.now() + 60000 });
    
    context.res = { json: data, headers: { "X-Cache": "MISS" } };
};

Monitoring Cold Starts

// Track cold start times
export const handler: AzureFunction = async (context: HttpRequest): Promise<void> => {
    const startTime = Date.now();
    
    // Check if this is a cold start
    const isColdStart = context.req.headers.get("x-functions-init") !== null;
    
    try {
        // Your logic
        const result = await processRequest(context.req);
        
        const duration = Date.now() - startTime;
        
        context.res = {
            json: {
                result,
                metrics: {
                    duration,
                    coldStart: isColdStart,
                    timestamp: new Date().toISOString()
                }
            }
        };
        
        // Log for monitoring
        context.log(`Request completed in ${duration}ms, coldStart: ${isColdStart}`);
    }
    catch (error) {
        context.log.error(`Error: ${error}`);
        throw error;
    }
};

Best Practices Summary

TechniqueImpactComplexity
Premium Plan (Always On)HighLow
Connection PoolingMediumMedium
Lazy InitializationMediumLow
Pre-warming TimerMediumLow
Optimize Bundle SizeHighMedium
Memory ManagementHighMedium
Async HTTP ClientsMediumLow

Summary