Skip to content

Serverless Development Patterns

Your image-processing Lambda fires twice on the same S3 upload, so half your thumbnails get written, deleted, and re-written — and CloudWatch is a wall of unstructured console.log you can’t correlate to a request. Meanwhile p99 latency spikes to 4 seconds every morning because of cold starts, and the “fix” someone shipped (provisioned concurrency on every function) doubled the bill. Serverless removes the servers, not the distributed-systems problems: retries, idempotency, cold starts, and per-platform limits are now your code.

This recipe shows how to drive Cursor, Claude Code, and Codex to generate serverless code that survives those problems — not the happy-path demo that breaks the first time SQS redelivers a message.

  • A copy-paste prompt that produces an idempotent AWS Lambda handler (dedupe on retries) with structured CloudWatch logging
  • A Cloudflare Worker API-gateway pattern with a real wrangler.toml, plus how the Cloudflare MCP server inspects KV/R2/D1 without leaving your editor
  • A cold-start triage workflow that tells you when provisioned concurrency is actually worth the money
  • The three-tool split: when to use Cursor’s agent, Claude Code’s headless runs, and Codex Cloud for deploy PRs
  • The failure modes that bite in production — duplicate SQS deliveries, DynamoDB hot partitions, Workers CPU limits — and the prompts that pre-empt them

Step 1: Scaffold the service (where the three tools differ)

Section titled “Step 1: Scaffold the service (where the three tools differ)”

The scaffolding step is where tool ergonomics diverge most. Cursor edits files visually in your repo; Claude Code runs the framework CLI from the terminal and is scriptable in CI; Codex can fan the same task out to a cloud worktree and open a deploy PR. Pick by where the work lives, not by capability — all three drive the same AWS SAM / Serverless Framework / Wrangler CLIs.

Open Agent mode (Cmd/Ctrl + I) in an empty repo and give it a concrete, single service to build:

Set up a Serverless Framework v4 service in TypeScript targeting AWS eu-central-1. One HTTP Lambda behind API Gateway (GET /health), one S3-triggered Lambda for image processing, and a DynamoDB table media (PK id). Use esbuild bundling, Node 22 runtime, and put stage/region in serverless.yml provider vars. Add a dev and prod stage.

The agent writes serverless.yml, the handler stubs, and package.json. Review the diff in the sidebar before accepting — Cursor checkpoints each edit so you can roll back a bad file without losing the rest.

Step 2: Make the handler idempotent (the part demos skip)

Section titled “Step 2: Make the handler idempotent (the part demos skip)”

S3, SQS, and EventBridge all deliver at least once. A naive handler reprocesses the same object on a retry. The fix is a dedupe key — for S3, the object’s eTag — checked against a conditional write before doing the work. This is the single most valuable thing to put in your prompt, so make it explicit:

That prompt produces something close to this — note the early return on a duplicate and the structured log, which is what makes it debuggable in production:

// handler.ts — the AI-generated shape worth reviewing for
import { S3Event } from 'aws-lambda';
import { DynamoDBClient, PutItemCommand } from '@aws-sdk/client-dynamodb';
const ddb = new DynamoDBClient({});
export const handler = async (event: S3Event) => {
const start = Date.now();
for (const rec of event.Records) {
const { bucket, object } = rec.s3;
const pk = `${bucket.name}/${object.key}#${object.eTag}`;
const log = { requestId: rec.responseElements['x-amz-request-id'], bucket: bucket.name, key: object.key, eTag: object.eTag };
try {
await ddb.send(new PutItemCommand({
TableName: process.env.DEDUPE_TABLE!,
Item: { pk: { S: pk } },
ConditionExpression: 'attribute_not_exists(pk)',
}));
} catch (err: any) {
if (err.name === 'ConditionalCheckFailedException') {
console.log(JSON.stringify({ ...log, status: 'duplicate-skipped' }));
continue; // already processed — S3 redelivery
}
throw err;
}
// ... resize with sharp, write derived/ objects ...
console.log(JSON.stringify({ ...log, status: 'ok', durationMs: Date.now() - start }));
}
};

The thing to verify in the AI’s output: that the dedupe write happens before the side effects, and that the catch block rethrows (so the DLQ actually catches failures) instead of swallowing the error.

On Workers the constraints are different — no Node runtime, a CPU-time budget per request, and bindings instead of SDK clients. Ask for the gateway and the config together so the wrangler.toml bindings match the code:

# wrangler.toml — the AI should produce bindings that match the Worker
name = "api-gateway"
main = "src/index.ts"
compatibility_date = "2026-06-01"
[vars]
ORIGIN = "https://origin.internal.example.com"
[[kv_namespaces]]
binding = "TOKENS"
id = "<your-kv-id>"
[[durable_objects.bindings]]
name = "RATE_LIMITER"
class_name = "RateLimiter"
[[migrations]]
tag = "v1"
new_sqlite_classes = ["RateLimiter"]

Step 4: Inspect live infra with the Cloudflare MCP server

Section titled “Step 4: Inspect live infra with the Cloudflare MCP server”

Generating the Worker is half the job; the bugs live in the binding values — a mistyped KV namespace id, a stale D1 row. Without MCP, you alt-tab between the dashboard and your editor. With Cloudflare’s managed remote MCP servers, the AI reads your real account state and the docs in the same conversation. Setup is identical across all three tools — point them at the remote endpoints (full server catalog and OAuth flow in the Cloudflare MCP guide):

{
"mcpServers": {
"cloudflare-bindings": {
"command": "npx",
"args": ["-y", "mcp-remote", "https://bindings.mcp.cloudflare.com/mcp"]
},
"cloudflare-observability": {
"command": "npx",
"args": ["-y", "mcp-remote", "https://observability.mcp.cloudflare.com/mcp"]
}
}
}

Before MCP, debugging the stale-cache bug meant copying namespace ids between four browser tabs. After: “Read the TOKENS KV namespace bound to the api-gateway Worker and list the keys written in the last hour” and the AI answers from your live account. For deploys and binding edits from the CLI, the lighter-weight alternative is the Cloudflare wrangler skill — install it with npx skills add cloudflare/skills/wrangler (from the cloudflare/skills repo, listed on skills.sh). A skill is the better fit when you just want the agent to know wrangler commands and conventions; the MCP server wins when you need it to read live state.