Deploying an Agent to Vercel or AWS Lambda Serverless Functions
Serverless functions run your agent handler on demand with little idle cost.
They fit stateless, short-to-medium turns when wall-clock work stays under the platform timeout and tools are remote APIs.
Summary
Package a single request handler that loads config from env, runs a bounded agent step, returns JSON (or streams within limits), and fails closed on missing secrets - then deploy the same idea to Vercel Functions or AWS Lambda with platform-specific adapters.
Recipe
- Confirm a realistic P95 duration fits under the function max timeout with margin (verify current Vercel/Lambda limits at build).
- Design the handler as stateless: read request, call models/tools, write external state, return.
- Bound the agent: max turns, hard timeout, and max tokens so loops cannot burn the whole budget.
- Load secrets only from the platform secret store / env; never commit keys.
- Implement a health or warm path if you use probes; keep it free of model calls.
- Choose packaging: Node/TS handlers on Vercel are common; Python on Lambda via ZIP, container image, or a framework adapter (Mangum for ASGI).
- Set memory/CPU high enough for your dependency import graph; cold starts grow with package size.
- Deploy to a preview/staging project first; smoke-test one turn with production-like secrets.
- Configure logs, tracing, and alerts for timeouts, 429/402 from providers, and error rates.
- If work exceeds timeouts, enqueue to a worker instead of stretching the function (Scaling Agent Systems Basics).
Working Example
Shared agent core (Python)
# agent_core.py
import os
import httpx
MODEL = os.environ.get("AGENT_MODEL", "openai/gpt-4o-mini") # verify at build
API_KEY = os.environ["OPENROUTER_API_KEY"] # fail if missing in prod path
MAX_CHARS = 4000
async def run_turn(message: str) -> dict:
message = message[:MAX_CHARS]
async with httpx.AsyncClient(timeout=45.0) as client:
r = await client.post(
"https://openrouter.ai/api/v1/chat/completions",
headers={
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json",
},
json={
"model": MODEL,
"messages": [
{"role": "system", "content": "Be concise. One short answer."},
{"role": "user", "content": message},
],
"max_tokens": 300,
},
)
r.raise_for_status()
data = r.json()
text = data["choices"][0]["message"]["content"]
return {"reply": text, "model": data.get("model", MODEL)}AWS Lambda + Function URL style (async via asyncio)
# lambda_handler.py
import asyncio
import json
from agent_core import run_turn
def handler(event, context):
# API Gateway HTTP API / Function URL payload shapes vary - normalize carefully
body = event.get("body") or "{}"
if event.get("isBase64Encoded"):
import base64
body = base64.b64decode(body).decode("utf-8")
try:
payload = json.loads(body)
except json.JSONDecodeError:
return {"statusCode": 400, "body": json.dumps({"error": "invalid json"})}
message = (payload.get("message") or "").strip()
if not message:
return {"statusCode": 400, "body": json.dumps({"error": "message required"})}
try:
result = asyncio.run(run_turn(message))
except Exception as exc: # log exc server-side
return {"statusCode": 502, "body": json.dumps({"error": "upstream_failed"})}
return {
"statusCode": 200,
"headers": {"content-type": "application/json"},
"body": json.dumps(result),
}Package with a deployment tool your team standardizes (AWS SAM, CDK, Terraform, or zip + console). Set OPENROUTER_API_KEY and AGENT_MODEL in the function configuration. Align timeout (for example 60s) and memory with measured needs.
Vercel (TypeScript handler pattern)
Vercel is often TypeScript-first for web agents; the shape matches the Python core: one request, env secrets, bounded model call.
// app/api/agent/route.ts (pattern - verify App Router conventions at build)
import { NextRequest, NextResponse } from "next/server";
export const maxDuration = 60; // verify plan limits at build
export async function POST(req: NextRequest) {
const { message } = await req.json();
if (!message || typeof message !== "string") {
return NextResponse.json({ error: "message required" }, { status: 400 });
}
const key = process.env.OPENROUTER_API_KEY;
if (!key) {
return NextResponse.json({ error: "misconfigured" }, { status: 500 });
}
const r = await fetch("https://openrouter.ai/api/v1/chat/completions", {
method: "POST",
headers: {
Authorization: `Bearer ${key}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: process.env.AGENT_MODEL ?? "openai/gpt-4o-mini",
messages: [
{ role: "system", content: "Be concise." },
{ role: "user", content: message.slice(0, 4000) },
],
max_tokens: 300,
}),
});
if (!r.ok) {
return NextResponse.json({ error: "upstream_failed" }, { status: 502 });
}
const data = await r.json();
return NextResponse.json({
reply: data.choices?.[0]?.message?.content ?? "",
model: data.model,
});
}Set secrets with vercel env (or the dashboard). Deploy with vercel deploy / git integration. Prefer preview URLs for eval before production.
Deep Dive
What "stateless" means here
Each invocation may hit a cold or warm worker.
Do not store conversation solely in module globals. Use cookies + DB, client-sent history with size caps, or a session store.
Timeouts and product design
| Strategy | Pattern |
|---|---|
| Fit under cap | One-shot or few-step agents only |
| Stream partial | SSE/stream within the same limit |
| Async handoff | Accept job → queue → worker → webhook |
| Split tools | Precompute retrieval outside the hot path |
See Cold Starts and Long-Running Agent Loops: A Serverless Mismatch.
Cold starts
Large dependency trees and container images slow first request after idle.
Mitigations: smaller packages, provisioned concurrency (Lambda), fewer heavy imports at module top-level, keep-alive traffic only when cost-justified.
Vercel vs Lambda (practical)
| Concern | Vercel Functions | AWS Lambda |
|---|---|---|
| Web/DX | Strong git + preview workflow | Broader AWS integration |
| Python | Supported paths evolve - verify | Mature ZIP/container options |
| Timeouts | Plan-dependent max duration | Configurable up to service max |
| Networking | Platform defaults | VPC for private resources (cold-start cost) |
| Auth to AWS services | Via external SDKs | IAM roles native |
Pick based on team surface area, not slogans.
Security basics
- Least-privilege IAM roles on Lambda; no long-lived AWS keys in env when roles work.
- Scope model API keys per environment (Scoping API Keys and Credentials Passed to an Agent).
- Validate and size-limit inputs; functions are public attack surface when exposed.
Gotchas
- Silent timeout kills mid-tool-loop with little client detail - log and surface "deadline".
- Oversized deployment packages - slow cold starts and deploy failures.
- VPC-attached Lambda without private NAT/interface endpoints - surprise outbound failures and longer cold starts.
- Streaming past platform limits - connection drops look like model bugs.
- Using one shared key across preview and prod - preview apps leak or overspend.
- Retrying non-idempotent tools on client timeout while the function still runs - double side effects.
- Loading huge frameworks for a single completion - prefer thin handlers at the edge.
- Ignoring
/tmpand memory caps when writing tool artifacts.
Alternatives
| Target | Use when |
|---|---|
| Cloud Run / scale-to-zero containers | Need Docker deps with request billing |
| Always-on container or VPS | Long loops, websockets, heavy tools |
| Queue + worker | Jobs longer than any function budget |
| Edge runtime only | Ultra-light routing; not multi-step Python agents |
FAQs
Can multi-step ReAct agents run on Lambda?
Yes if the whole loop fits the timeout and memory. Many production systems still offload long ReAct to workers.
Is Python on Vercel a good default?
Teams building Next.js UIs often keep the agent in TypeScript on Vercel or call a Python worker elsewhere. Verify current Python support for your plan at build.
How do I local-test Lambda?
Use SAM CLI, LocalStack-style tooling, or pure unit tests on run_turn with mocked HTTP. Always smoke-test once deployed.
What timeout should I set?
Start from measured P95 × headroom, capped by product SLO and plan max. Do not default to maximum without cost controls.
How do secrets differ from Docker hosts?
Same principle, different UI: platform env/secret stores vs orchestrator secrets. See Environment and Secrets Management Across Deployment Targets.
Should every agent endpoint be public?
No. Put auth (session, API key, IAM, Cloudflare Access, etc.) in front. Public demos need strict rate limits and cheap models.
Do cold starts break streaming chat UX?
They delay first byte after idle. Warm capacity or non-serverless gateways fix UX when SLOs are tight.
Can I use provisioned concurrency forever?
You can, but you pay for readiness. Use it for latency-critical paths, not every batch job.
Related
Related: Choosing a Deployment Target for an Agent Workload
Related: Cold Starts and Long-Running Agent Loops: A Serverless Mismatch
Related: Containerizing an Agent with Docker
Related: Environment and Secrets Management Across Deployment Targets
Related: Deploying Agents Best Practices
Stack versions: Pins from the category manifest (verify at build): OpenRouter (~315+ models, July 2026 pricing/fees); LangGraph 1.0+; CrewAI 1.14+; Microsoft Agent Framework 1.0; Vercel AI SDK 6; Pydantic AI (latest); LlamaIndex (latest); OpenAI Agents SDK (latest + MCP); MCP (Linux Foundation governance); A2A (HTTP+SSE+JSON-RPC 2.0); Solana
@solana/web3.js+@solana/spl-token.