Azure Functions — Cold Start Prevention and Performance Optimization
The Problem
Your Azure Functions experience:
- First request after idle takes 5-15 seconds
- Users complain about slow response times
- Functions scale out too slowly during traffic spikes
- Memory usage spikes cause timeouts
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
| Technique | Impact | Complexity |
|---|---|---|
| Premium Plan (Always On) | High | Low |
| Connection Pooling | Medium | Medium |
| Lazy Initialization | Medium | Low |
| Pre-warming Timer | Medium | Low |
| Optimize Bundle Size | High | Medium |
| Memory Management | High | Medium |
| Async HTTP Clients | Medium | Low |
Summary
- Use Premium plan with always-ready instances for critical functions
- Implement lazy initialization to delay loading
- Reuse connections across invocations
- Monitor cold start times and optimize the slowest functions
- Pre-warm functions with timer triggers