Skip to content
serverless-architecture logo

Building Serverless Applications

serverless-architecture

Load when building serverless applications with AWS Lambda, Vercel Functions, or Cloudflare Workers. Applies when implementing FaaS patterns, managing cold starts, or optimizing serverless costs.

telum-ai/speck0installs2stars

SKILL.md

Full skill instructions

Building Serverless Applications

Follow these patterns for serverless architecture. Covers cold starts, connection pooling, function composition, and cost optimization.

🔌 MCP Servers (Cloudflare)

If using Cloudflare Workers, they provide official MCP servers:

Where to put this config (Speck template repos):

  • Add the snippet to .cursor/​mcp.project.json.example (create if missing; committed, no secrets)
  • Then run: bash .speck/​scripts/​bash/​merge-mcp-config.sh to generate .cursor/​mcp.json (local)
{
  "mcpServers": {
    "cloudflare-workers": {
      "type": "http",
      "url": "https://bindings.mcp.cloudflare.com/mcp"
    },
    "cloudflare-observability": {
      "type": "http",
      "url": "https://observability.mcp.cloudflare.com/mcp"
    }
  }
}

Available servers: Workers Bindings, Builds, Observability, Containers, Browser Rendering, Logpush, AI Gateway, and more. See Cloudflare MCP docs.


When This Rule Applies

Apply when building with AWS Lambda, Vercel Functions, Cloudflare Workers, or similar FaaS platforms.


Cold Start Mitigation

Keep Functions Warm

// Provisioned concurrency (AWS Lambda)
// Configure in serverless.yml or AWS Console

// Or use scheduled warming
// CloudWatch rule: rate(5 minutes)
export const warmer = async (event) => {
  if (event.source === 'serverless-plugin-warmup') {
    console.log('Warming up');
    return 'warmed';
  }
  // ... normal handler
};

Minimize Package Size

// Use tree-shakeable imports
import { DynamoDB } from '@aws-sdk/​client-dynamodb';  // ✓
// NOT: import AWS from 'aws-sdk';                    // ✗

// Bundle with esbuild/​rollup
// Target: node18, external: ['@aws-sdk/​*']

Initialize Outside Handler

// ✓ GOOD: Initialized once per container
const db = new DynamoDB();
const stripe = new Stripe(process.env.STRIPE_KEY);

export const handler = async (event) => {
  // db and stripe already initialized
  return await db.query(...);
};

// ✗ BAD: Initialized on every invocation
export const handler = async (event) => {
  const db = new DynamoDB();  // Cold start every time
  return await db.query(...);
};

Database Connection Pooling

The Problem

Serverless functions can spawn hundreds of concurrent instances, each opening its own database connection → connection exhaustion.

Solutions

SolutionPlatformUse Case
Connection poolerAnyPostgreSQL (PgBouncer, Supabase)
HTTP-based DBAnyPlanetscale, Neon, Supabase
Serverless adapterPrisma@prisma/​adapter-neon
Data APIAWSAurora Serverless Data API

Prisma with Connection Pooling

import { PrismaClient } from '@prisma/​client';
import { neonConfig, Pool } from '@neondatabase/​serverless';
import { PrismaNeon } from '@prisma/​adapter-neon';

neonConfig.fetchConnectionCache = true;

const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const adapter = new PrismaNeon(pool);
const prisma = new PrismaClient({ adapter });

export const handler = async (event) => {
  const users = await prisma.user.findMany();
  return { statusCode: 200, body: JSON.stringify(users) };
};

Supabase with Pooler

// Use pooler URL instead of direct connection
const supabase = createClient(
  process.env.SUPABASE_URL,
  process.env.SUPABASE_KEY,
  {
    db: {
      // Use transaction pooler for serverless
      schema: 'public',
    },
  }
);

Function Patterns

Single Responsibility

// ✓ GOOD: One function per operation
// functions/​createUser.ts
// functions/​getUser.ts
// functions/​updateUser.ts

// ✗ BAD: Monolithic handler
export const handler = async (event) => {
  switch (event.httpMethod) {
    case 'GET': return getUser(event);
    case 'POST': return createUser(event);
    case 'PUT': return updateUser(event);
    // ... becomes unmaintainable
  }
};

API Gateway Pattern

// Vercel: app/​api/​users/[id]/​route.ts
export async function GET(
  request: Request,
  { params }: { params: { id: string } }
) {
  const user = await db.user.findUnique({ where: { id: params.id } });
  return Response.json(user);
}

export async function PUT(
  request: Request,
  { params }: { params: { id: string } }
) {
  const data = await request.json();
  const user = await db.user.update({ where: { id: params.id }, data });
  return Response.json(user);
}

Async Processing

Queue-Based (AWS)

// Producer: API function
export const createOrder = async (event) => {
  const order = await db.order.create({ data: event.body });
  
  await sqs.sendMessage({
    QueueUrl: process.env.ORDERS_QUEUE_URL,
    MessageBody: JSON.stringify({ orderId: order.id }),
  });
  
  return { statusCode: 202, body: 'Processing' };
};

// Consumer: Queue-triggered function
export const processOrder = async (event) => {
  for (const record of event.Records) {
    const { orderId } = JSON.parse(record.body);
    await fulfillOrder(orderId);
  }
};

Background Jobs (Vercel)

// Use waitUntil for fire-and-forget
export async function POST(request: Request) {
  const data = await request.json();
  
  // Respond immediately
  const response = Response.json({ status: 'accepted' });
  
  // Continue processing after response
  waitUntil(
    sendWelcomeEmail(data.email)
      .then(() => updateAnalytics(data))
  );
  
  return response;
}

Environment Configuration

Per-Stage Config

# serverless.yml
provider:
  environment:
    NODE_ENV: ${opt:stage, 'dev'}
    
custom:
  database:
    dev: ${ssm:/​app/​dev/​database-url}
    prod: ${ssm:/​app/​prod/​database-url}

functions:
  api:
    environment:
      DATABASE_URL: ${self:custom.database.${opt:stage, 'dev'}}

Secrets Management

// AWS Secrets Manager
import { SecretsManager } from '@aws-sdk/​client-secrets-manager';

const sm = new SecretsManager();
let cachedSecret: string;

async function getSecret(name: string): Promise<string> {
  if (cachedSecret) return cachedSecret;
  
  const response = await sm.getSecretValue({ SecretId: name });
  cachedSecret = response.SecretString!;
  return cachedSecret;
}

Cost Optimization

Memory/​Duration Tradeoff

Higher memory = more CPU = faster execution = potentially cheaper.

# Test different memory settings
functions:
  api:
    memorySize: 1024  # 1GB - often sweet spot
    timeout: 10

Avoid Unnecessary Invocations

// Use API Gateway caching
// Configure in AWS Console or serverless.yml

// Or implement at code level
const cache = new Map();

export const handler = async (event) => {
  const cacheKey = event.pathParameters.id;
  
  if (cache.has(cacheKey)) {
    return { statusCode: 200, body: cache.get(cacheKey) };
  }
  
  const data = await fetchData(cacheKey);
  cache.set(cacheKey, JSON.stringify(data));
  
  return { statusCode: 200, body: JSON.stringify(data) };
};

Common Gotchas

Timeout Mismatches

API Gateway: 29s max, Lambda: 15 min max. Set Lambda timeout < API Gateway timeout.

Cold Start Spikes

Schedule warmers, use provisioned concurrency for latency-critical functions.

Connection Leaks

Always use connection pooling or HTTP-based databases.

Large Payloads

Lambda payload limit: 6MB. Use S3 for large files:

// Generate presigned URL for upload
const uploadUrl = await s3.getSignedUrl('putObject', {
  Bucket: 'uploads',
  Key: `uploads/​${uuid()}`,
  Expires: 300,
});
return { uploadUrl };

Execution Context Reuse

Don't rely on global state persisting, but do reuse expensive initializations.


Quick Reference

TaskPattern
Reduce cold startInitialize outside handler
Connection poolingPgBouncer / Neon / Data API
Background workSQS queue or waitUntil
SecretsAWS Secrets Manager / SSM
Cost optimizeTest memory settings, use caching
Large payloadsS3 presigned URLs

References