Developing Serverless Functions for Website (AWS Lambda)
AWS Lambda — functions that run on event and don't need constantly running server. Pay only for actual execution time: first 1M requests monthly free, then $0.20 per 1M requests. Cold start is the only serious drawback, solved several ways.
When Lambda Fits for Website
Fits: contact form processing, PDF/image generation on request, webhooks from payment systems and CRM, image resizing on upload, task schedulers (cron via EventBridge), API proxy for third-party services.
Doesn't fit: long-lived connections (WebSocket needs separate service), tasks > 15 minutes, high-frequency DB operations without connection pooling.
Project Structure
my-lambda/
├── src/
│ ├── handlers/
│ │ ├── contact-form.ts
│ │ ├── image-resize.ts
│ │ └── webhook.ts
│ ├── services/
│ │ ├── email.ts
│ │ └── storage.ts
│ └── utils/
├── template.yaml # SAM or serverless.yml
├── package.json
└── tsconfig.json
Example Function: Contact Form Processing
import { APIGatewayProxyHandler } from "aws-lambda";
import { SESClient, SendEmailCommand } from "@aws-sdk/client-ses";
import { z } from "zod";
const ses = new SESClient({ region: "eu-west-1" });
const ContactSchema = z.object({
name: z.string().min(2).max(100),
email: z.string().email(),
message: z.string().min(10).max(2000),
});
export const handler: APIGatewayProxyHandler = async (event) => {
const headers = {
"Access-Control-Allow-Origin": "https://your-site.com",
"Content-Type": "application/json",
};
try {
const body = JSON.parse(event.body || "{}");
const data = ContactSchema.parse(body);
await ses.send(new SendEmailCommand({
Source: "[email protected]",
Destination: { ToAddresses: ["[email protected]"] },
Message: {
Subject: { Data: `New message from ${data.name}` },
Body: {
Text: { Data: `From: ${data.name} <${data.email}>\n\n${data.message}` }
}
}
}));
return { statusCode: 200, headers, body: JSON.stringify({ ok: true }) };
} catch (error) {
if (error instanceof z.ZodError) {
return { statusCode: 400, headers, body: JSON.stringify({ errors: error.errors }) };
}
console.error(error);
return { statusCode: 500, headers, body: JSON.stringify({ error: "Internal error" }) };
}
};
Deploy via AWS SAM
template.yaml:
AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Globals:
Function:
Runtime: nodejs20.x
Timeout: 10
MemorySize: 256
Environment:
Variables:
NODE_ENV: production
Resources:
ContactFormFunction:
Type: AWS::Serverless::Function
Properties:
Handler: dist/handlers/contact-form.handler
Events:
Api:
Type: HttpApi
Properties:
Path: /contact
Method: POST
Policies:
- SESCrudPolicy:
IdentityName: your-site.com
npm run build
sam build
sam deploy --guided # first deploy
sam deploy # subsequent
Cold Start and Mitigation
Cold start on Node.js 20 — 200–500 ms. Python — 100–300 ms. After first call function stays "warm" minutes.
Mitigation ways:
- Provisioned Concurrency — Lambda keeps N instances always warm. Costs ~$0.015 per hour per instance. For critical endpoints with SLA.
-
Reduce bundle size — use esbuild instead webpack, don't include
aws-sdkin bundle (already in environment), tree-shaking. - SnapStart (Java) — snapshot state after initialization.
- Warmer — EventBridge calls function every 5 minutes with warmup-payload.
// Initialization outside handler — executes once on cold start
const dbClient = new DynamoDBClient({ region: "eu-west-1" });
const sesClient = new SESClient({ region: "eu-west-1" });
export const handler = async (event) => {
// handler uses already initialized clients
};
Working with Database
Standard TCP connection to PostgreSQL/MySQL doesn't fit Lambda — each call creates new connection, at 1000 RPS database chokes.
Solutions:
- RDS Proxy — connection pool before RDS, Lambda connects to proxy. +$0.015 per vCPU/hour RDS.
- DynamoDB — no connections, natively serverless. For simple data structures.
- PlanetScale / Neon — serverless-compatible databases with HTTP API.
Logging and Tracing
import { Logger } from "@aws-lambda-powertools/logger";
import { Tracer } from "@aws-lambda-powertools/tracer";
const logger = new Logger({ serviceName: "contact-form" });
const tracer = new Tracer({ serviceName: "contact-form" });
export const handler = tracer.captureLambdaHandler(async (event) => {
logger.addContext(context);
logger.info("Processing contact form", { email: event.body?.email });
// ...
});
AWS Lambda Powertools — official library for structured logging, tracing via X-Ray, metrics via CloudWatch EMF.
Timeline
One Lambda function with SAM deploy — 1–2 days. Set of 5–7 functions with CI/CD via GitHub Actions and monitoring — 5–7 days.







