Serverless Introduction
Deploy functions without managing servers - AWS Lambda, Vercel Functions, Cloudflare Workers.
4 min read
What is Serverless?#
Serverless lets you run code without provisioning or managing servers. You pay only for what you use.
Traditional: Server runs 24/7 → Pay for uptime
Serverless: Function runs on request → Pay per execution
Serverless vs Traditional#
| Aspect | Serverless | Traditional Server |
|---|---|---|
| Scaling | Automatic | Manual/configured |
| Cost | Per execution | Per hour/month |
| Cold starts | Yes (few hundred ms) | No |
| Max execution | Limited (15min max) | Unlimited |
| State | Stateless | Can be stateful |
| Maintenance | None | Patches, updates |
When to Use Serverless#
Good For#
- APIs - REST/GraphQL endpoints
- Webhooks - Event handlers
- Cron jobs - Scheduled tasks
- Image processing - On-demand transformations
- Authentication - Token validation
- Low traffic - Intermittent usage
Not Ideal For#
- Long-running processes - >15 min tasks
- WebSockets - Persistent connections (except some platforms)
- Consistent low latency - Cold starts can add delay
- Heavy computation - CPU-intensive tasks
Serverless Platforms#
| Platform | Best For | Cold Start | Price |
|---|---|---|---|
| AWS Lambda | AWS ecosystem | ~200-500ms | $0.20/1M requests |
| Vercel Functions | Next.js/frontend | ~50-200ms | Free tier + usage |
| Cloudflare Workers | Edge, low latency | ~0ms | Free tier + usage |
| Netlify Functions | JAMstack | ~200-400ms | Free tier + usage |
| Google Cloud Functions | GCP ecosystem | ~200-500ms | $0.40/1M requests |
| Azure Functions | Microsoft stack | ~200-500ms | $0.20/1M requests |
Basic Function Structure#
All serverless functions follow a similar pattern:
javascript
// Request → Handler → Response
export async function handler(request) {
// 1. Parse input
const data = await request.json();
// 2. Do work
const result = await processData(data);
// 3. Return response
return new Response(JSON.stringify(result), {
headers: { 'Content-Type': 'application/json' },
});
}
Environment Variables#
All platforms support environment variables for secrets:
bash
# Set via CLI or dashboard
DATABASE_URL=postgres://...
API_KEY=sk_...
JWT_SECRET=...
javascript
// Access in function
const apiKey = process.env.API_KEY;
Statelessness#
Serverless functions are stateless. Don't rely on:
- Global variables persisting between requests
- Local file system (may be ephemeral)
- In-memory caches lasting forever
javascript
// BAD - State may not persist
let cache = {};
export async function handler(req) {
cache[req.id] = 'value'; // May be lost
}
// GOOD - Use external storage
import { redis } from './redis';
export async function handler(req) {
await redis.set(req.id, 'value'); // Persisted
}
Cold Starts#
First request after inactivity is slower:
Request 1: [Cold Start: 500ms] + [Execution: 50ms] = 550ms
Request 2: [Warm] + [Execution: 50ms] = 50ms
Request 3: [Warm] + [Execution: 50ms] = 50ms
... (idle for 15 minutes)
Request 4: [Cold Start: 500ms] + [Execution: 50ms] = 550ms
Minimizing Cold Starts#
- Keep functions small - Fewer dependencies to load
- Use lighter runtimes - Node.js is faster than Python
- Provision concurrency - AWS Lambda provisioned concurrency
- Edge functions - Cloudflare Workers have near-zero cold starts
- Keep warm - Scheduled pings (not recommended for cost)
javascript
// Lazy load heavy dependencies
let prisma;
export async function handler(req) {
if (!prisma) {
const { PrismaClient } = await import('@prisma/client');
prisma = new PrismaClient();
}
// Use prisma...
}
Project Structure#
/api
/users
route.js # /api/users
[id]/route.js # /api/users/:id
/webhooks
stripe/route.js # /api/webhooks/stripe
/auth
login/route.js # /api/auth/login
/lib
db.js # Shared database connection
auth.js # Auth utilities
/middleware
withAuth.js # Auth wrapper
Middleware Pattern#
Since serverless functions are isolated, use wrapper patterns:
javascript
// middleware/withAuth.js
export function withAuth(handler) {
return async (request) => {
const token = request.headers.get('Authorization')?.replace('Bearer ', '');
if (!token) {
return new Response(JSON.stringify({ error: 'Unauthorized' }), {
status: 401,
});
}
try {
const user = await verifyToken(token);
request.user = user;
return handler(request);
} catch {
return new Response(JSON.stringify({ error: 'Invalid token' }), {
status: 401,
});
}
};
}
// api/users/route.js
import { withAuth } from '@/middleware/withAuth';
export const GET = withAuth(async (request) => {
const users = await getUsers(request.user.id);
return Response.json({ data: users });
});
Key Takeaways#
- Pay per use - Great for variable traffic
- Auto-scaling - Handles traffic spikes
- Stateless - Use external storage for state
- Cold starts - Plan for initial latency
- Time limits - Not for long-running tasks
Continue Learning
Ready to level up your skills?
Explore more guides and tutorials to deepen your understanding and become a better developer.