Serverless Development Patterns
PLServerless development patterns for AI coding agents are recipes that make Claude Code, Codex and Cursor produce AWS Lambda, Cloudflare Workers and Vercel code that survives production: handlers that stay idempotent when events are redelivered, Worker bindings that match wrangler.toml, and cold-start fixes that come before provisioned concurrency. Each recipe ends in a test or gate, not a manual read.
This page is for backend developers who already ship functions and now let an agent write most of them. The situation it fixes: your image-processing Lambda fires twice on the same S3 upload, CloudWatch is a wall of unstructured console.log, and the p99 spike every morning was “fixed” by provisioned concurrency on every function, which doubled the bill. Serverless removes the servers, not the distributed-systems problems. Retries, idempotency, cold starts and per-platform limits are still your code, and an agent writes the happy path unless you ask for more.
The recipes assume a TypeScript project with Vitest; if you do not have that toolchain yet, set it up with Node.js patterns first, or pick another stack from the backend recipes hub.
What you’ll walk away with from these serverless recipes
Section titled “What you’ll walk away with from these serverless recipes”- A prompt that produces an idempotent S3-to-Lambda handler, plus the corrected handler shape to check it against.
- A test prompt that proves the handler survives a duplicate delivery and a mid-flight failure, so you verify behaviour instead of reading the diff.
- A Cloudflare Worker API gateway prompt with a matching
wrangler.toml, and two commands that catch binding drift and bundle bloat before deploy. - Per-tool setup for the Cloudflare MCP server and the Cloudflare skills, with the context cost of the MCP server.
- A cold-start triage prompt that tells you when provisioned concurrency is worth paying for.
- The failure modes that reach production (duplicate SQS deliveries, recursive S3 triggers, Workers CPU limits, stuck idempotency claims) and how to recover from each.
How do you scaffold a serverless service with an agent?
Section titled “How do you scaffold a serverless service with an agent?”Give the agent one concrete service, a region, a runtime and the event sources. All three tools drive the same framework CLIs (Serverless Framework, AWS SAM, Wrangler), so pick the tool by where the work lives, not by capability. The scaffold prompt below uses Serverless Framework v4 (serverless 4.43.0 on npm, checked 2026-09-26) and Node.js 22.
Run from the empty repo in a terminal. For the first run, pass --permission-mode manual to approve each edit yourself (from v2.1.283, on both release channels on 2026-10-12, interactive sessions otherwise start in auto mode). Switch to --permission-mode acceptEdits, as below, once you trust the plan.
claude --permission-mode acceptEdits "Set up a Serverless Framework v4 service in TypeScript for AWS eu-central-1: one HTTP Lambda behind API Gateway (GET /health), one S3-triggered image Lambda that only fires for the uploads/ prefix, and a DynamoDB table 'media' (PK id) plus a 'dedupe' table with TTL on 'ttl'. esbuild bundling, Node.js 22 runtime, dev and prod stages. Add a vitest setup and an npm 'test' script. Do not deploy."Use claude -p with the same prompt when the scaffold has to be reproducible in a script.
Keep writes inside the workspace and let Codex ask before anything wider:
codex --sandbox workspace-write -a on-request "Set up a Serverless Framework v4 service in TypeScript for AWS eu-central-1: one HTTP Lambda behind API Gateway (GET /health), one S3-triggered image Lambda that only fires for the uploads/ prefix, a DynamoDB table 'media' (PK id) and a 'dedupe' table with TTL on 'ttl'. esbuild, Node.js 22, dev and prod stages, vitest with an npm 'test' script. Do not deploy."Permission profiles (-c default_permissions=":workspace", beta) are the newer control; --sandbox still works in codex-cli 0.157.1. Do not combine the two.
To run the same task in Codex Cloud and review a branch instead, codex cloud exec --env ENV_ID "<same prompt>" submits it (--env is required in codex-cli 0.157.1; list environments with codex cloud).
Paste the same prompt into Cursor’s agent in the open repository and review the proposed diff file by file before you accept it. For a larger scaffold, ask for a plan first with Plan Mode, then let the agent execute it.
How do you make an AWS Lambda handler idempotent?
Section titled “How do you make an AWS Lambda handler idempotent?”S3, SQS and EventBridge deliver events at least once. A handler with side effects (writes, emails, charges) must claim a stable key before it acts, skip the work when the claim already exists, and release the claim when the work fails, so the retry can run. Generated handlers often get the first two right and miss the third: they write the dedupe record, crash in sharp, and every retry is then skipped as a “duplicate”. The object is never processed.
Check the output against this shape. The order of operations is what matters, not the style:
// handler.ts: claim, work, release on failureimport type { S3Event } from 'aws-lambda';import { DynamoDBClient, PutItemCommand, DeleteItemCommand } from '@aws-sdk/client-dynamodb';import { resizeAndWrite } from './resize';
const ddb = new DynamoDBClient({});const TABLE = process.env.DEDUPE_TABLE!;
export const handler = async (event: S3Event) => { for (const rec of event.Records) { const start = Date.now(); const bucket = rec.s3.bucket.name; const key = decodeURIComponent(rec.s3.object.key.replace(/\+/g, ' ')); const pk = `${bucket}/${key}#${rec.s3.object.eTag}`; const log = { requestId: rec.responseElements['x-amz-request-id'], bucket, key, pk };
try { await ddb.send(new PutItemCommand({ TableName: TABLE, Item: { pk: { S: pk }, ttl: { N: String(Math.floor(start / 1000) + 7 * 86400) } }, ConditionExpression: 'attribute_not_exists(pk)', })); } catch (err) { if ((err as Error).name !== 'ConditionalCheckFailedException') throw err; console.log(JSON.stringify({ ...log, status: 'duplicate-skipped' })); continue; }
try { await resizeAndWrite(bucket, key); console.log(JSON.stringify({ ...log, status: 'ok', durationMs: Date.now() - start })); } catch (err) { await ddb.send(new DeleteItemCommand({ TableName: TABLE, Key: { pk: { S: pk } } })); console.error(JSON.stringify({ ...log, status: 'failed', error: (err as Error).message })); throw err; } }};A timeout can still kill the function between the claim and the release, which leaves a claim that blocks every retry. If you would rather not hand-roll a lease with an expiry, ask the agent to use @aws-lambda-powertools/idempotency (2.35.0 on npm, checked 2026-09-26) instead: it stores an in-progress record with its own expiry and a completed record with the result, which covers that case.
How do you prove the handler survives a duplicate delivery?
Section titled “How do you prove the handler survives a duplicate delivery?”Do not read the handler line by line. Write down the four behaviours that matter and make a test fail if any of them breaks. Tell the agent the tests are the specification, so a failing test leads to a handler fix and not to an edited assertion. Protect the oracle explains why that instruction matters.
Then run the gates. Each one goes red for a specific bug, which is what lets you trust the result without reviewing every line:
| Gate | Command (terminal or CI) | Goes red when |
|---|---|---|
| Behaviour tests | npx vitest run | A duplicate is processed twice, the claim is not released after a failed resize, or a key is not decoded |
| Types | npx tsc --noEmit | The handler reads a field the event type does not have |
| Package | npx serverless package --stage dev | serverless.yml does not parse or a handler path is wrong |
| Worker bindings | npx wrangler types, then npx tsc --noEmit | Worker code reads a binding that wrangler.toml does not declare |
| Worker bundle | npx wrangler deploy --dry-run --outdir dist | The printed Total Upload exceeds the size budget a CI step compares it against (for example, fail above 1 MiB); the dry run itself never fails on size, and nothing is uploaded |
| Production signal | CloudWatch Logs Insights: stats count(*) by status | The duplicate-skipped or failed count moves after a release |
You own the tests and the gates. Changes to IAM policies, on-failure destinations and stage configuration go to a human reviewer, because a wrong IAM grant passes every test above. Deploys run from CI, not from an agent session. The evidence bundle page shows how to attach the gate results to the pull request.
To have the agent run the gates and fix what fails without supervision:
claude -p "Run npx vitest run and npx tsc --noEmit. If anything fails, fix src/, never tests/, and rerun until both pass. Print which behaviour was wrong and what you changed." \ --permission-mode acceptEdits \ --allowedTools "Bash(npx vitest *)" "Bash(npx tsc *)" \ --disallowedTools "Edit(tests/**)" "Write(tests/**)"codex exec --sandbox workspace-write "Run npx vitest run and npx tsc --noEmit. If anything fails, fix src/, never tests/, and rerun until both pass. Print which behaviour was wrong and what you changed."Paste the same instruction into the agent. To run it on every pull request instead, use a Cloud Agent automation with a Source control trigger, and let Bugbot review the resulting pull request, as described in Cursor CI/CD.
The prompt alone only asks the agent to leave tests/ alone. In Claude Code, --disallowedTools makes that a rule the session cannot break. For Codex and Cursor, enforce it outside the agent: a CODEOWNERS entry that requires a human review for tests/, or a CI check that fails when an agent branch changes files under tests/.
How do you build a Cloudflare Worker API gateway with matching bindings?
Section titled “How do you build a Cloudflare Worker API gateway with matching bindings?”Workers have no Node.js runtime by default, a CPU-time budget per request, and bindings instead of SDK clients. Ask for the Worker and its config in one prompt so the binding names in code and in wrangler.toml come from the same answer.
# wrangler.toml: binding names must match the Worker's Env typename = "api-gateway"main = "src/index.ts"compatibility_date = "2026-09-01"
[vars]ORIGIN = "https://origin.internal.example.com"
[[kv_namespaces]]binding = "TOKENS"id = "KV_NAMESPACE_ID"
[[durable_objects.bindings]]name = "RATE_LIMITER"class_name = "RateLimiter"
[[migrations]]tag = "v1"new_sqlite_classes = ["RateLimiter"]Replace KV_NAMESPACE_ID with the id that npx wrangler kv namespace create TOKENS prints. Then run npx wrangler types: it regenerates the Env type from this file, so a Worker that reads env.TOKEN instead of env.TOKENS fails tsc instead of failing at runtime.
How do you give the agent live Cloudflare state?
Section titled “How do you give the agent live Cloudflare state?”Generated code is half the job. The other half is the state it runs against: a mistyped namespace id, a stale KV value, a route that throws in production. Cloudflare offers two ways to put that in front of the agent, and they solve different problems.
- The Cloudflare MCP server reads and changes your live account. The recommended Code Mode server at
https://mcp.cloudflare.com/mcpexposes three tools (search,execute,docs) over the whole Cloudflare API. Cloudflare puts its schema cost at about 1,000 tokens, against about 244,000 for native tool schemas of the same API (Cloudflare,cloudflare/mcpREADME, read 2026-09-26). Results are capped at about 6,000 tokens by default. Domain servers such ashttps://observability.mcp.cloudflare.com/mcpcover one area each. - The Cloudflare skills (
cloudflare/skills, 14 skills for Workers, Wrangler, Durable Objects and more) teach the agent the platform’s conventions. The plugin form bundles the same Code Mode MCP server.
The MCP server needs a URL entry per tool. Authentication is OAuth on first use, or a Cloudflare API token sent as a bearer token.
claude mcp add --transport http cloudflare https://mcp.cloudflare.com/mcp# or the plugin, which brings the skills and the MCP server together:# /plugin marketplace add cloudflare/skills# /plugin install cloudflare@cloudflareLeave out --transport http and Claude Code 2.1.283 still saves the URL as a stdio command. It prints a warning, but the entry is written and fails at connect time.
codex mcp add cloudflare --url https://mcp.cloudflare.com/mcp# or the plugin:codex plugin marketplace add cloudflare/skillscodex plugin add cloudflare@cloudflareAdd the server to .cursor/mcp.json:
{ "mcpServers": { "cloudflare": { "url": "https://mcp.cloudflare.com/mcp" } } }For the skills alone, npx skills add https://github.com/cloudflare/skills installs them for Cursor and other agents.
Before MCP, finding a stale token meant copying namespace ids between dashboard tabs. With it, one prompt answers from the live account:
Pick the skill when the agent only needs to know wrangler commands and conventions. Pick the MCP server when it has to read or change live state, and keep write access out of sessions where you do not need it. For AWS, the equivalent is AWS’s own plugin set (aws/agent-toolkit-for-aws, with an aws-core plugin for serverless and CDK work). In Claude Code, install it with /plugin install aws-core@claude-plugins-official. In Codex, run codex plugin marketplace add aws/agent-toolkit-for-aws, then install aws-core from /plugins. The full server list and OAuth details are on the Cloudflare MCP page.
More serverless prompts: cold starts, queues and Vercel
Section titled “More serverless prompts: cold starts, queues and Vercel”What breaks when agent-written serverless code reaches production?
Section titled “What breaks when agent-written serverless code reaches production?”Where to go next with serverless
Section titled “Where to go next with serverless”Did this guide help you?
One tap, no sign-in.
What will you do with it?
How did it read?
What got in the way? Pick any that apply
Your role?
Thanks. I read every answer. Michał Jaskólski
Did this guide help?
One tap each. Close whenever you like.
no sign-in