AgentSkillsCN

serverless-architecture

在利用AWS Lambda、Vercel Functions或Cloudflare Workers构建无服务器应用时加载此模块。适用于实施FaaS模式、管理冷启动问题,或优化无服务器架构的成本。

SKILL.md
--- frontmatter
name: serverless-architecture
description: 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.

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)
json
{
  "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

typescript
// 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

typescript
// 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

typescript
// ✓ 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

typescript
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

typescript
// 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

typescript
// ✓ 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

typescript
// 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)

typescript
// 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)

typescript
// 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

yaml
# 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

typescript
// 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.

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

Avoid Unnecessary Invocations

typescript
// 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:

typescript
// 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