Contact Form Processing with AWS Lambda
Contact form on a React site with 2000+ submissions per day started failing: response came after 30 seconds, emails were lost. The old monolithic EC2 instance couldn't handle the spikes. We replaced it with AWS Lambda — response time dropped to 300 ms, cost per form submission — fractions of a cent. Serverless architecture eliminates server management: you focus on the code, and AWS scales the load. However, cold start and database operations require careful design. We'll cover typical scenarios, show code, and provide ready-made solutions. To evaluate your project, contact our certified AWS engineers with 12+ years of experience and 80+ serverless projects delivered.
Lambda vs EC2: When to Use Which
Lambda is efficient for asynchronous and episodic tasks. Here's a comparison with a classic EC2 server:
| Criteria | AWS Lambda | AWS EC2 |
|---|---|---|
| Server management | Not required | Full management |
| Scaling | Automatic | Manual / Autoscaling |
| Cold start | 100–500 ms | None |
| Price | Per execution | Per uptime |
| Time limit | 15 minutes | Unlimited |
Suitable for: contact form processing, on-demand PDF/image generation, webhooks from payment systems and CRM, image resizing on upload, task schedulers (cron via EventBridge), API proxies for third-party services.
Not suitable for: long-lived connections (WebSocket requires a separate service), tasks exceeding 15 minutes, high-frequency DB operations without connection pooling. In such cases, Lambda becomes not only more expensive but also slower: at 5000 req/s, EC2 with autoscaling costs less than Provisioned Concurrency.
Why Lambda Is More Cost-Effective
From a cost perspective, Lambda wins under uneven load. On EC2, you pay for a running server 24/7, even if it processes requests once per hour. Lambda charges per invocation — savings on idle time can reach 90%. The official AWS documentation gives an example: a site with 10,000 requests per day on EC2 t3.micro costs ~$8/month, while Lambda costs $0.05 for the same workload — a 99% reduction. Additionally, you avoid configuring autoscaling, patching the OS, and monitoring. Our clients save up to 70% on infrastructure by moving to Lambda.
How Lambda Processes a Form: Code Example
A contact form on a React site sends a POST request to API Gateway, which triggers a Lambda function. The function validates data with Zod and sends an email via SES. Code:
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://www.example.org",
"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" }) };
}
};
Optimizing Lambda Performance
How to Reduce Cold Start?
Cold start is the initialization latency after a long idle period. On Node.js 20 it's 200–500 ms, on Python 100–300 ms. If sub-second response time is critical, use one of these methods:
| Method | Description | Additional cost |
|---|---|---|
| Provisioned Concurrency | N pre-warmed instances | $0.015 per instance/hour |
| esbuild + tree-shaking | Reduce bundle size | Free |
| SnapStart (Java) | Snapshot after initialization | Free |
| Ping requests | EventBridge every 5 minutes | Negligible |
Example of initializing clients outside the handler (executed once):
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
};
Common Developer Mistakes
- Overly large bundle: including all dependencies without tree-shaking increases cold start by 200–500 ms.
- Creating clients inside the handler: each invocation creates a new DB connection — leads to N+1 problem.
- Ignoring timeout limits: if the function runs longer than timeout (max 15 min), it fails.
- Lack of error handling: uncaught exceptions lead to retries and additional costs.
How to Connect Lambda to a Database?
Standard TCP connection to PostgreSQL/MySQL in Lambda creates a new connection per invocation — at 1000 RPS, the database chokes. Solutions:
| Solution | Features | Price |
|---|---|---|
| RDS Proxy | Connection pool in front of RDS | +$0.015 per vCPU/hour |
| DynamoDB | Native serverless, no connections | Per request |
| PlanetScale / Neon | Serverless DB with HTTP API | Pay per use |
For a high number of concurrent invocations, RDS Proxy can become a bottleneck — in such cases, migrate to DynamoDB or shard the database.
Deployment and Monitoring
Development and Deployment Process
- Design: define triggers, IAM roles, timeouts, and memory.
- Development: write the function in TypeScript with validation and error handling.
- Local testing: run with SAM CLI or Docker with API Gateway emulation.
- Build: use esbuild for bundling (tree-shaking, minification).
- Deploy: via AWS SAM (template.yaml) — first
sam deploy --guided, thensam deploy. - Monitoring: set up CloudWatch dashboards, alerts for errors and execution time.
We use AWS Serverless Application Model (SAM) documentation for infrastructure as code. Example template.yaml:
AWSTemplateFormatVersion: "2010-10-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: example.org
Commands:
npm run build
sam build
sam deploy --guided
sam deploy
Logging and Tracing
AWS Lambda Powertools is the official library for structured logging, tracing via X-Ray, and metrics via CloudWatch EMF.
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 });
// ...
});
Our Services
What's Included in Our Work
- Architectural design: choose triggers, balance cold start
- Develop functions in TypeScript with validation and error handling
- Set up CI/CD via GitHub Actions: build, tests, deploy
- API documentation and infrastructure description
- Monitoring: CloudWatch dashboards, alerts for errors and execution time
- Team training: code review, maintenance guide
Timeline
A single Lambda function with SAM deployment — 1–2 days. A set of 5–7 functions with CI/CD and monitoring — 5–7 days. Our engineers have 12+ years of AWS experience and have delivered 80+ serverless projects. Exact timelines depend on integrations. Get a consultation on the solution architecture — we'll prepare a detailed turnkey proposal.







