---
name: vercel-functions
description: Vercel Functions expert guidance — Node.js/Bun/Python runtimes, Fluid Compute, long-duration (30 min) functions, large functions (5 GB bundles), Docker/OCI container images, plan limits, streaming, WebSockets, and Cron Jobs. Use when configuring, debugging, or optimizing server-side code running on Vercel.
summary: "Vercel Functions run on Fluid Compute with Node.js as the default runtime — strongly prefer it over `runtime = 'edge'` (Vercel recommends migrating off Edge, and Next.js 16.3+ no longer supports it). Duration: 300s default on every plan including Hobby, 800s max on Pro/Enterprise, 1800s (30 min) per-function in the extended beta; beyond that use Vercel Workflow. Bundles: 250 MB standard (500 MB Python), 5 GB via the large functions beta (`VERCEL_SUPPORT_LARGE_FUNCTIONS=1`). Request/response bodies cap at 4.5 MB. Memory is dashboard-only (Standard 2 GB/1 vCPU, Performance 4 GB/2 vCPU; Hobby fixed). Docker works: add `Dockerfile.vercel` to run an OCI image as an autoscaling, stateless, scale-to-zero Function."
metadata:
  priority: 8
  docs:
    - "https://vercel.com/docs/functions"
    - "https://vercel.com/docs/functions/runtimes"
    - "https://vercel.com/docs/functions/limitations"
    - "https://vercel.com/docs/functions/configuring-functions/duration"
    - "https://vercel.com/docs/functions/container-images"
    - "https://vercel.com/docs/fluid-compute"
    - "https://vercel.com/docs/functions/websockets"
  sitemap: "https://vercel.com/sitemap.xml"
  pathPatterns:
    - 'api/**/*.*'
    - 'pages/api/**'
    - 'src/pages/api/**'
    - 'app/**/route.*'
    - 'src/app/**/route.*'
    - 'apps/*/api/**/*.*'
    - 'apps/*/app/**/route.*'
    - 'apps/*/src/app/**/route.*'
    - 'apps/*/pages/api/**'
    - 'vercel.json'
    - 'apps/*/vercel.json'
    - 'vercel.ts'
    - 'apps/*/vercel.ts'
    # Vercel builds `Dockerfile.vercel` / `Containerfile.vercel` into a
    # container-image Function, so these are Functions config, not generic Docker.
    - 'Dockerfile.vercel'
    - '*/Dockerfile.vercel'
    - 'Containerfile.vercel'
    - '*/Containerfile.vercel'
  bashPatterns:
    - '\bvercel\s+dev\b'
    - '\bvercel\s+logs\b'
    - '\bvercel\s+vcr\b'
  importPatterns:
    - 'ws'
    - 'socket.io'
    - 'socket.io-client'
  promptSignals:
    phrases:
      - "websocket"
      - "websockets"
      - "web socket"
      - "socket.io"
      # Polling is the classic technique people reach for when they think Vercel
      # lacks websockets. We intentionally do NOT trigger on named third-party
      # services (Pusher, PubNub, Ably) — those are deliberate choices, not a
      # signal that someone is working around a missing feature.
      - "long polling"
      - "long-polling"
      # Duration: people hit the ceiling and ask about it in these words.
      - "maxduration"
      - "max duration"
      - "function timeout"
      - "function times out"
      - "long-running function"
      - "long running function"
      - "504"
      # Bundle size: the 250 MB error message is the usual entry point.
      - "unzipped maximum size"
      - "250mb"
      - "250 mb"
      - "large function"
      - "bundle size limit"
      # Containers: Dockerfile.vercel is a Functions feature, not just packaging.
      - "dockerfile"
      - "container image"
      - "container registry"
      - "runtime edge"
      - "edge runtime"
    allOf:
      - ["docker", "vercel"]
      - ["hobby", "limit"]
    anyOf:
      - "realtime"
      - "bidirectional"
      - "ws server"
      - "polling"
      - "server-sent events"
      - "fluid compute"
      - "active cpu"
    noneOf: []
    minScore: 6
validate:
  -
    pattern: export\s+default\s+function
    message: 'Use named exports (GET, POST, PUT, DELETE) instead of default export for route handlers'
    severity: error
    # Skip on App Router page / layout / loading / error / not-found / sitemap / template / default files,
    # which require a default export by Next.js convention. Detected via the 'use client' directive,
    # an App Router config export (metadata, dynamic, revalidate, fetchCache, runtime), an `export default
    # function` whose name matches an App Router file (Page / Layout / Loading / etc.), or any JSX
    # element with a capitalised component tag — all signals that the file is a page-style file rather
    # than a route handler. See anthropics/claude-code#54989.
    skipIfFileContains: "(?:^|\\n)\\s*['\"]use\\s+client['\"]|export\\s+const\\s+(?:metadata|dynamic|revalidate|fetchCache|runtime)\\b|export\\s+default\\s+(?:async\\s+)?function\\s+\\w*(?:Page|Layout|Loading|Error|NotFound|Sitemap|Template|Default|sitemap|robots|opengraph|manifest)\\b|<[A-Z][A-Za-z0-9]*|\\{\\s*children\\s*[,}:]|MetadataRoute\\.|from\\s+['\"]next/(?:font|image|link|navigation|headers|cookies)['\"]"
  -
    pattern: NextApiRequest|NextApiResponse
    message: 'NextApiRequest/NextApiResponse are Pages Router types — use Web API Request/Response'
    severity: error
  -
    # Vercel's docs recommend migrating off the Edge runtime, and Next.js 16.3+
    # doesn't support it. Surfaced as a recommendation rather than an error:
    # existing Edge functions still work, so this is a nudge, not a blocker.
    # Matches `"runtime": "edge"` in vercel.json too.
    pattern: 'runtime[''"]?\s*[=:]\s*[''"]edge[''"]'
    message: 'Consider dropping `runtime = "edge"`. Vercel recommends migrating from Edge to Node.js, and Next.js 16.3+ no longer supports it. Node.js on Fluid Compute runs in the same regions at the same price with full Node.js APIs, longer durations, and larger bundles.'
    severity: recommended
  -
    pattern: 'from\s+[''"](openai|@anthropic-ai/sdk|anthropic)[''"]|new\s+(OpenAI|Anthropic)\('
    message: 'Direct AI provider SDK detected in route handler. Use the Vercel AI SDK for streaming, tools, and provider abstraction.'
    severity: recommended
    upgradeToSkill: ai-sdk
    upgradeWhy: 'Replace vendor-locked provider SDKs with @ai-sdk/openai or @ai-sdk/anthropic for unified streaming and tool support.'
    skipIfFileContains: '@ai-sdk/|from\s+[''"](ai)[''"]|import.*from\s+[''"](ai)[''"]|streamText|generateText'
  -
    pattern: 'setTimeout\s*\(|setInterval\s*\(|await\s+new\s+Promise\s*\([^)]*setTimeout'
    message: 'Long-running or polling logic detected in a serverless handler. Functions have execution time limits.'
    severity: recommended
    upgradeToSkill: workflow
    upgradeWhy: 'Move delayed/polling logic to Vercel Workflow for durable execution with pause, resume, retries, and crash safety.'
    skipIfFileContains: 'use workflow|use step'
  -
    pattern: 'writeFile(Sync)?\(|createWriteStream\(|from\s+[''"](multer|formidable)[''"]|fs\.writeFile'
    message: 'Local filesystem write detected. Serverless functions have ephemeral, read-only filesystems.'
    severity: error
    upgradeToSkill: vercel-storage
    upgradeWhy: 'Replace local filesystem writes with Vercel Blob, Neon, or Upstash for persistent, platform-native storage.'
    skipIfFileContains: '@vercel/blob|@upstash/|@neondatabase/'
  -
    pattern: 'export\s+(async\s+)?function\s+(GET|POST|PUT|PATCH|DELETE)\b'
    message: 'Route handler has no observability instrumentation. Add logging and error tracking for production debugging.'
    severity: warn
    skipIfFileContains: 'console\.error|logger\.|captureException|Sentry|@vercel/otel|withTracing'
  -
    pattern: 'from\s+[''""](lru-cache|node-cache|memory-cache)[''""]|new\s+(LRUCache|NodeCache|Map)\(\s*\).*cache'
    message: 'In-process memory cache detected in serverless function. Process memory is not shared across invocations.'
    severity: recommended
    upgradeToSkill: runtime-cache
    upgradeWhy: 'Replace in-process caches with Vercel Runtime Cache (getCache from @vercel/functions) for region-aware caching that persists across invocations.'
    skipIfFileContains: 'getCache|from\s+[''""]\@vercel/functions[''""]'
  -
    pattern: 'maxRetries\s*[=:]|retryCount\s*[=:]|retry\s*\(\s*|for\s*\([^)]*retry|while\s*\([^)]*retry'
    message: 'Manual retry logic detected. Use Vercel Workflow SDK for automatic retries with durable execution.'
    severity: recommended
    upgradeToSkill: workflow
    upgradeWhy: 'Replace manual retry loops with Workflow SDK steps that provide automatic retries, crash safety, and observability.'
    skipIfFileContains: 'use workflow|use step|from\s+[''""](workflow)[''""]'
  -
    pattern: 'from\s+[''"](express)[''""]|require\s*\(\s*[''"](express)[''""\)]'
    message: 'Express.js detected in a Vercel project. Vercel Functions use the Web Request/Response API — Express middleware, req/res, and app.listen() do not work in serverless.'
    severity: recommended
    upgradeToSkill: vercel-functions
    upgradeWhy: 'Replace Express with Next.js route handlers (export async function GET/POST) or Vercel Functions using the Web Request/Response API.'
    skipIfFileContains: 'export\s+(async\s+)?function\s+(GET|POST|PUT|PATCH|DELETE)|from\s+[''""](next/server|@vercel/functions)[''""]'
retrieval:
  aliases:
    - serverless functions
    - api routes
    - edge functions
    - lambda
    - websockets
    - socket.io
    - docker
    - dockerfile
    - container images
    - function timeout
    - max duration
    - bundle size
    - hobby limits
  intents:
    - create serverless function
    - configure function runtime
    - optimize cold starts
    - add api route
    - serve a websocket connection
    - run a function for longer than 5 minutes
    - deploy a dockerfile
    - fix a function that exceeds the bundle size limit
    - check plan limits for functions
  entities:
    - Serverless Functions
    - Edge Functions
    - Fluid Compute
    - Long-duration functions
    - Large functions
    - Container Images
    - Vercel Container Registry
    - streaming
    - WebSockets
    - Cron Jobs
chainTo:
  -
    pattern: 'from\s+[''\"](openai|@anthropic-ai/sdk|anthropic)[''"]|new\s+(OpenAI|Anthropic)\('
    targetSkill: ai-sdk
    message: 'Direct AI provider SDK in route handler — loading AI SDK guidance for unified streaming and tool support.'
  -
    pattern: 'setTimeout\s*\(|setInterval\s*\(|await\s+new\s+Promise\s*\([^)]*setTimeout'
    targetSkill: workflow
    message: 'Long-running or polling logic in serverless handler — loading Workflow SDK for durable execution.'
  -
    pattern: 'writeFile(Sync)?\(|createWriteStream\(|from\s+[''\"](multer|formidable)[''"]|fs\.writeFile'
    targetSkill: vercel-storage
    message: 'Local filesystem write in serverless function — loading Vercel Storage guidance for platform-native persistence.'
  -
    pattern: 'from\s+[''""]@vercel/(postgres|kv)[''""]'
    targetSkill: vercel-storage
    message: '@vercel/postgres and @vercel/kv are sunset — loading Vercel Storage guidance for Neon and Upstash migration.'
  -
    pattern: 'generateObject\s*\(|streamObject\s*\(|toDataStreamResponse|maxSteps\b|CoreMessage\b'
    targetSkill: ai-sdk
    message: 'Deprecated AI SDK v5 API detected — loading AI SDK guidance for migration.'
  -
    pattern: 'while\s*\(\s*true\s*\)\s*\{|for\s*\(\s*;\s*;\s*\)\s*\{|setInterval\s*\(\s*async'
    targetSkill: workflow
    message: 'Polling loop in serverless function detected — loading Workflow SDK for durable, crash-safe execution with pause/resume.'
    skipIfFileContains: "use workflow|use step|from\\s+['\"]workflow['\"]"
  -
    pattern: "from\\s+['\"]express['\"]|require\\s*\\(\\s*['\"]express['\"]"
    targetSkill: vercel-functions
    message: 'Express.js detected — loading Vercel Functions guidance for Web Request/Response API route handlers that replace Express middleware and routing.'
    skipIfFileContains: "export\\s+(async\\s+)?function\\s+(GET|POST|PUT|PATCH|DELETE)"
  -
    pattern: 'from\s+[''""](lru-cache|node-cache|memory-cache)[''""]|new\s+(LRUCache|NodeCache|Map)\(\s*\).*cache'
    targetSkill: runtime-cache
    message: 'In-process memory cache in serverless function — loading Runtime Cache guidance for region-aware caching that persists across invocations.'
    skipIfFileContains: 'getCache|from\s+[''""]\@vercel/functions[''""]'
  -
    pattern: 'maxRetries\s*[=:]|retryCount\s*[=:]|retry\s*\(\s*|for\s*\([^)]*retry|while\s*\([^)]*retry'
    targetSkill: workflow
    message: 'Manual retry logic in serverless handler — loading Workflow SDK guidance for automatic retries with durable execution.'
    skipIfFileContains: 'use workflow|use step|from\s+[''""](workflow)[''""]'

  -
    pattern: 'import\s*\{[^}]*\bmetric\b[^}]*\}\s*from\s*[''"]@vercel/functions[''"]'
    targetSkill: custom-metrics
    message: '@vercel/functions metric() detected — loading Custom Metrics guidance for naming, attributes, and vc metrics queries.'

---

# Vercel Functions

You are an expert in Vercel Functions — the compute layer of the Vercel platform.

## Prefer Node.js over the Edge runtime

**Strongly prefer Node.js. Don't reach for `runtime = 'edge'` in new code, and recommend migrating off it when you find it in existing code.**

This follows Vercel's own documented guidance — *"we recommend migrating from edge to Node.js for improved performance and reliability"* — and one hard constraint: as of **Next.js 16.3, `runtime = 'edge'` is no longer supported**. Routes and pages there run on Node.js regardless of what you write, so on 16.3+ this stops being a recommendation and becomes a migration you have to do.

Everywhere else it is a strong default, not a prohibition. Both runtimes run on the same Fluid Compute infrastructure, in the same regions, under the same Active CPU pricing — so in nearly every case Edge gains you nothing while costing you most of the Node.js API surface. If you have a specific, tested reason to stay on Edge, that's a legitimate call; just make it deliberately rather than by habit.

### The default to reach for

```ts
// app/api/hello/route.ts — no runtime export needed.
export async function GET() {
  return Response.json({ message: 'Hello from Node.js on Fluid Compute' })
}
```

Node.js is the default. Omit `export const runtime` entirely rather than writing `export const runtime = 'nodejs'`.

### Reasons people reach for Edge — and what to do instead

| "I need Edge because…" | Reality | Do this instead |
|---|---|---|
| "…I need to stream / SSE / AI tokens" | Streaming is zero-config on Node.js. This is the single most common false belief. | Return a `ReadableStream` from a normal Node.js function |
| "…I need low latency" | Both run on Fluid Compute. Fluid pre-warms instances and caches bytecode; the difference is noise next to your DB/API round trips | Stay on Node.js; pin `regions` near your data |
| "…auth checks / redirects / A-B tests at the edge" | That's Routing Middleware's job, and **Routing Middleware supports full Node.js** — it is not edge-only | Use Routing Middleware (`routing-middleware` skill) |
| "…it's cheaper" | Identical Active CPU pricing | Stay on Node.js |
| "…it has faster cold starts" | Fluid Compute reuses warm instances across concurrent invocations and bytecode-caches Node 20+ in production | Stay on Node.js |
| "…my function must run globally" | Edge's global execution usually *hurts* — every DB query crosses an ocean | Single region (`iad1` default) next to your database |

### What Edge actually costs you

- No `fs`, no native modules, no `require()` — ESM only, and most npm packages with Node.js dependencies simply will not load
- No `eval` / `new Function` / dynamic `WebAssembly.instantiate`
- **Code size limit after gzip: 1 MB (Hobby), 2 MB (Pro), 4 MB (Enterprise)** — versus 250 MB uncompressed (up to 5 GB) on Node.js
- Must begin sending a response within **25 seconds** (it may then stream for up to 300s). The 300s/800s/1800s duration limits below apply to the Node.js, Bun, and Python runtimes — **not** to Edge
- No long-duration or large-function support of any kind

### Migrating an existing Edge function

Worth doing when you're already touching the file, and required on Next.js 16.3+. An Edge function that works today isn't an emergency.

1. Remove `export const runtime = 'edge'` (or `runtime: 'edge'` in `vercel.json` / the `config` object).
2. Replace `next/server` Edge-only imports where applicable; the Web `Request`/`Response` handler signature is unchanged, so most route handlers need no other edit.
3. If you pinned execution with the Edge-only `preferredRegion`, use `regions` in `vercel.json` instead.
4. Confirm Fluid Compute is on (default since April 23, 2025) and redeploy.

There is no rollback story to plan for: Node.js is a superset of what the function could do on Edge.

## Function Types

### Node.js (the default)
- Full Node.js runtime, all npm packages available
- Default for Next.js route handlers, Server Actions, Server Components, and any file in `/api`
- **Node.js 24 LTS is GA** for builds and functions (V8 13.6, global `URLPattern`, Undici v7, npm v11). **Node.js 20 is deprecated on October 1, 2026** — move off `nodejs20.x`
- Duration: 300s default on every plan; 800s max on Pro/Enterprise; 1800s with the extended-duration beta

### Bun
Add `"bunVersion": "1.x"` to `vercel.json` to run functions on Bun instead of Node.js. ~28% lower latency for CPU-bound workloads. Supports Next.js, Express, Hono, Nitro, and `Bun.serve` as an entrypoint. Bun supports both large functions and extended max duration.

### Python
Python 3.12 / 3.13 / 3.14 on Fluid Compute. FastAPI, Flask, and Django build into a **single** function from the resolved entrypoint — key `vercel.json` config on that entrypoint file (`app/main.py`, `myproject/wsgi.py`), not on `/api` routes. Python gets a **500 MB** standard bundle limit (vs. 250 MB) and supports large functions and extended duration.

### Rust
Rust functions run on Fluid Compute with HTTP streaming and Active CPU pricing. Official runtime (Beta) built on the `vercel_runtime` crate. Supports environment variables up to 64 KB.

### Container images (Docker)
Any OCI image via `Dockerfile.vercel`. See [Docker and Container Images](#docker-and-container-images) below.

### Edge (legacy — not recommended)
V8 isolates with a subset of Web APIs. Fine to leave in place on existing deployments, but not the runtime to pick for new work. See [Prefer Node.js over the Edge runtime](#prefer-nodejs-over-the-edge-runtime).

### Choosing a Runtime

| Need | Runtime | Why |
|------|---------|-----|
| Anything not listed below | `nodejs` | The default, and correct nearly always |
| Full Node.js APIs, npm packages | `nodejs` | Full compatibility |
| AI streaming, SSE, WebSockets | `nodejs` | Zero-config streaming, long durations |
| Lower latency, CPU-bound work | `nodejs` + Bun | ~28% latency reduction |
| Database connections, heavy deps | `nodejs` | Pin `regions` next to the database |
| Data/ML libraries, big model files | `nodejs` or `python` + large functions | Up to 5 GB bundles |
| Systems-level performance | `rust` | Native speed on Fluid Compute |
| Custom system libraries (FFmpeg, Chromium), Go/Ruby/PHP, unsupported frameworks | container image | Bring your own Dockerfile |
| Auth, redirects, A/B tests before the cache | Routing Middleware | Runs on Node.js, framework-agnostic |
| Hours-to-months of execution | Vercel Workflow | Durable steps, no duration limit |

`edge` is deliberately absent: there's no row here where it's the better answer for new code.

## Fluid Compute

Fluid Compute is the execution model for Vercel Functions — **enabled by default for new projects since April 23, 2025**, and available for the Node.js, Python, Bun, Rust, and Edge runtimes. Enable it explicitly per-deployment with `{"fluid": true}` in `vercel.json`, or project-wide in Settings → Functions.

Long-duration, large-function, and container-image support all depend on it.

Key behaviors:
- **Optimized concurrency**: multiple invocations share one instance instead of one microVM per request. Vercel prioritizes idle existing resources before allocating new ones. Available on the Node.js and Python runtimes.
- **Active CPU pricing**: you are billed for CPU time your code actually consumes, plus provisioned memory while requests are in flight, plus invocations. Waiting on I/O (AI models, DB queries) does not accrue Active CPU — which is what makes 30-minute functions affordable.
- **Automatic cold start optimization**: function pre-warming plus **bytecode caching** on Node.js 20+. Bytecode caching applies to **production only** — not dev or preview, so don't benchmark cold starts in a preview deployment.
- **Error isolation**: an uncaught exception or unhandled rejection is logged and in-flight requests are allowed to finish; one broken request will not crash its neighbors on the same instance.
- **Cross-AZ and cross-region failover**: fails over to another availability zone in-region first, then to the next closest region.
- **Graceful shutdown**: `SIGTERM` before termination (see below).

### Instance Sizes (memory / CPU)

| Type | Memory / CPU | Use |
|------|--------------|-----|
| Standard (default) | 2 GB / 1 vCPU | Predictable performance for production workloads |
| Performance | 4 GB / 2 vCPU | Latency-sensitive applications and SSR workloads |

- **With Fluid Compute enabled, memory cannot be set in `vercel.json`** — setting it there produces a build-time warning. Set it in the dashboard instead: Settings → Functions → Advanced Settings → **Function CPU**, then redeploy. (The `memory` key still exists for legacy non-Fluid deployments, which is why you will find older examples using it.)
- **Pro/Enterprise only.** Hobby always runs Standard (2 GB / 1 vCPU) and cannot configure it. The Basic instance has been removed.
- More memory also means more CPU, which can *reduce* Active CPU billing for CPU-bound work by finishing sooner — but it raises Provisioned Memory cost while requests are in flight.
- Projects created before 2019-11-08 may still sit on legacy sizes (1024 MB / 0.6 vCPU on Hobby, 3008 MB / 1.67 vCPU on Pro) until you pick a size in the dashboard.

### Settings precedence

Function code (`export const maxDuration`) → `vercel.json` → dashboard → Fluid defaults. Later entries lose.

### Background Processing with `waitUntil`

`waitUntil` takes a **Promise**, not a callback. Passing a function does nothing — a common and silent bug.

```ts
import { waitUntil } from '@vercel/functions'

export async function POST(req: Request) {
  const data = await req.json()

  // Correct: invoke the async work and hand over the promise.
  waitUntil(processAnalytics(data))

  // For several tasks, combine them:
  waitUntil(Promise.all([sendNotification(data), updateCache(data)]))

  return Response.json({ received: true })
}
```

### Next.js `after` (equivalent)

```ts
import { after } from 'next/server'

export async function POST(req: Request) {
  const data = await req.json()

  after(async () => {
    await logToAnalytics(data)
  })

  return Response.json({ ok: true })
}
```

### Graceful shutdown and request cancellation

```ts
// Runs on scale-down. 500 ms to clean up (30 s for container images).
process.on('SIGTERM', () => {
  // flush buffers, close pools
})
```

Request cancellation is **opt-in**, per path. With it enabled, a client disconnect aborts `request.signal` and terminates the function — anything not wrapped in `waitUntil`/`after` is lost, which is exactly why it is not on by default.

```json filename="vercel.json"
{
  "functions": {
    "api/*": { "supportsCancellation": true }
  }
}
```

```ts
export async function GET(request: Request) {
  // Pass the signal through so upstream work stops too.
  const res = await fetch('https://upstream.example.com', { signal: request.signal })
  return new Response(res.body, { status: res.status })
}
```

## Duration and Long-Duration Functions

### Duration limits

With Fluid Compute (default), per [Vercel's limits](https://vercel.com/docs/functions/limitations#max-duration):

| Plan | Default | Maximum | Extended maximum |
|------|---------|---------|------------------|
| Hobby | 300s (5 min) | 300s (5 min) | — |
| Pro | 300s (5 min) | 800s | 1800s (30 min) — Beta |
| Enterprise | 300s (5 min) | 800s | 1800s (30 min) — Beta |

The 800s maximum is **generally available** on Pro and Enterprise. The 1800s extended maximum is **in beta**. Exceeding the limit returns `504 FUNCTION_INVOCATION_TIMEOUT`.

**Hobby's default and maximum are the same 300s** — there is no headroom to raise, and no extended duration. Setting `maxDuration` above 300s on Hobby does nothing; upgrade to Pro.

### Setting `maxDuration`

```ts
// app/api/report/route.ts — Next.js App Router (and Node.js, SvelteKit, Astro,
// Nuxt, Remix via their own config). Value is in seconds.
export const maxDuration = 800

export async function POST(request: Request) {
  return Response.json({ ok: true })
}
```

For other frameworks and runtimes — Next.js < 13.5, Rust, Go, Python, Ruby — use `vercel.json`:

```json
{
  "$schema": "https://openapi.vercel.sh/vercel.json",
  "functions": {
    "api/long-task.py": { "maxDuration": 1800 }
  }
}
```

Glob order matters, and Next.js projects using `src/` must prefix paths with `src/`. For Python frameworks, key on the resolved entrypoint (`app/main.py`), not an `/api` route.

To change the project-wide default: Settings → Functions → **Function Max Duration**.

### Extended max duration (30 minutes) — Beta

Pro and Enterprise teams can run individual functions for up to **1800s**. Requirements, all of which are load-bearing:

- **Per-function configuration only.** Durations above 800s must be set in code or in `vercel.json` for that function. **Project-level defaults above 800s are not supported** during the beta — raising the dashboard default will not get you to 1800s.
- **Supported runtimes only**: `nodejs20.x`, `nodejs22.x`, `nodejs24.x`, Bun `1.x` and `1.4.x`, `python3.12`, `python3.13`, `python3.14`.
- **Fluid Compute must be enabled** (default for new projects).
- **Secure Compute and Static IPs do not support durations above 800s** during the beta. If the project uses either, you are capped at 800s.

```ts
// app/api/long-task/route.ts
export const maxDuration = 1800 // 30 minutes

export async function POST(request: Request) {
  await doTheLongThing()
  return Response.json({ ok: true })
}
```

### Keeping a long request alive

Over HTTP/2, Vercel sends connection-level `PING` frames while the response is idle. **HTTP/1.1 has no equivalent**, so HTTP/1.1 clients and intermediate proxies may still close an idle connection long before 30 minutes elapse. For any long-running handler, **stream progress or heartbeat data while the work runs** rather than going silent and emitting one payload at the end.

Use `getDeadline()` to find out how much time is actually left and bail out cleanly:

```ts
import { getDeadline } from '@vercel/functions'

const msRemaining = getDeadline().getTime() - Date.now()
```

### Cost of long functions

Active CPU pricing is what makes this viable: a 25-minute function that spends 24 minutes awaiting an LLM bills almost no Active CPU, only Provisioned Memory for the instance while the request is in flight.

### When 30 minutes is not enough

Do not chain functions, self-invoke, or poll to fake durability. Use **Vercel Workflow**, which pauses, resumes, and keeps state for minutes to months with no duration limit, plus automatic retries and crash safety. Rough guide:

- ≤ 300s → any plan, no configuration needed
- 300–800s → Pro/Enterprise, set `maxDuration`
- 800–1800s → Pro/Enterprise, extended-duration beta, per-function config
- Beyond 30 min, or needs to survive a crash/deploy → Vercel Workflow (`workflow` skill)

Workflow steps themselves support extended function durations, so a single step can also run up to 30 minutes.

## Large Functions (bundle size)

### Standard limits

| Runtime | Uncompressed bundle limit |
|---------|---------------------------|
| Node.js, Bun, Rust, Go | 250 MB (includes runtime layers) |
| Python | 500 MB |
| Edge runtime | 1 MB Hobby / 2 MB Pro / 4 MB Enterprise, **after gzip** |

Blowing the limit fails the build with `Serverless Function has exceeded the unzipped maximum size of 250 MB`.

### Large functions — Beta

Large functions raise the uncompressed bundle ceiling to **5 GB**. This is what makes Python data/AI libraries, model weights, browser automation (Playwright/Puppeteer), image/video processing, and big backend apps deployable as Functions.

- **Runtimes**: Node.js, Bun, Python.
- **Requires Fluid Compute with Active CPU** enabled (default for new projects).
- **New projects are eligible by default.** Existing projects opt in with the `VERCEL_SUPPORT_LARGE_FUNCTIONS` environment variable, then redeploy:

```bash
vercel env add VERCEL_SUPPORT_LARGE_FUNCTIONS   # value: 1  (use 0 to disable)
```

The environment variable always takes precedence over the project default, in both directions.

- **Only functions that exceed the standard limit use the large-function path** — everything under 250 MB keeps the normal, faster path, so enabling it is not a global performance trade.
- **Not supported with Secure Compute or Static IPs.**

### Shrinking a bundle first

A 5 GB function still costs you cold-start time. Trim before you opt in:

In `vercel.json` (not supported in Next.js — see below):

```json filename="vercel.json"
{
  "functions": {
    "api/**/*.py": {
      "excludeFiles": "{tests/**,__tests__/**,**/*.test.py,fixtures/**,testdata/**}"
    }
  }
}
```

- Next.js ignores `includeFiles`/`excludeFiles` — use `outputFileTracingIncludes` / `outputFileTracingExcludes` in `next.config.js` instead.
- Audit heavy imports, prefer dynamic `import()`, and check for a package in `dependencies` that belongs in `devDependencies`.

### Request and response payloads

Bundle size is not payload size. The **request or response body of a Function is capped at 4.5 MB**; exceeding it returns `413 FUNCTION_PAYLOAD_TOO_LARGE`. For larger data:

- **Uploads** → Vercel Blob **client uploads**, which send the file browser → Blob directly, bypassing the function
- **Large responses** → stream them; streamed responses are not subject to the limit
- Otherwise, chunk across multiple requests

## Docker and Container Images

Vercel Functions run **OCI-compatible container images**. This is first-class Docker support: bring a Dockerfile, get an autoscaling function with scale-to-zero and Active CPU pricing. It is *not* a VM or a long-lived server.

### Quick start

Create `Dockerfile.vercel` (or `Containerfile.vercel`) at the project root. Vercel detects it automatically and adds a rewrite routing all traffic to the image.

```docker
# Dockerfile.vercel
FROM node:26-alpine

RUN npm i -g srvx
WORKDIR /app
COPY server.ts .

# srvx listens on $PORT by default
CMD ["srvx", "--prod"]
```

```ts
// server.ts
export default {
  fetch(req: Request) {
    return Response.json({ ip: req.headers.get('x-forwarded-for') })
  },
}
```

Deploy with `vercel deploy` or a Git push. During the build, the image is built and pushed to [Vercel Container Registry (VCR)](https://vercel.com/docs/container-registry).

### The rules that actually bite

- **Serve HTTP on port 80**, or override with the `PORT` environment variable in project settings. A container that doesn't listen gets no traffic.
- **Containers must be stateless.** Each instance takes a request, returns a response, and keeps nothing between calls — that is what allows autoscaling and scale-to-zero. Persist to a Marketplace database, Redis, or Blob; never to the container filesystem.
- **Scale to zero** after 5 minutes without traffic in production, 30 seconds in preview. Cold starts are real; do not assume a warm process.
- **`SIGTERM` with a 30-second grace period** on scale-down (regular functions get 500 ms). Use it to drain.
- **Logs are not per-request.** `stdout`/`stderr` are broadcast to all inflight requests of the instance, so correlate with your own request IDs.
- **Same Function limits and Active CPU pricing** apply for size, memory, and duration.
- **Secure Compute and Static IPs are not supported** with custom container images. If you need either, deploy that part without a container.
- **Local dev**: `vercel dev` runs the image and requires the `docker` CLI plus a running daemon.

### Multiple services in one project

Use [Services](https://vercel.com/docs/services) to deploy several frontends/backends in one project, containerized or not. Set `runtime: "container"` on any service you want built as an image; `entrypoint` points at the Dockerfile relative to that service's `root`.

```json filename="vercel.json"
{
  "services": {
    "frontend": { "runtime": "container", "root": "frontend/", "entrypoint": "Dockerfile.vercel" },
    "backend":  { "runtime": "container", "root": "backend/",  "entrypoint": "Dockerfile.vercel" }
  },
  "rewrites": [
    { "source": "/api/(.*)", "destination": { "service": "backend" } },
    { "source": "/(.*)",     "destination": { "service": "frontend" } }
  ]
}
```

Services are internal by default — without a top-level rewrite, nothing is publicly routable. When `services` is present, build/runtime keys (`functions`, `buildCommand`, `installCommand`, `outputDirectory`, `framework`) move into the service and are no longer valid at the top level.

### Vercel Container Registry (VCR)

```bash
vercel vcr login docker              # authenticate Docker with a short-lived OIDC token
vercel vcr image ls my-app           # list images
vercel vcr image inspect my-app <id>
vercel vcr image rm my-app <id>
```

Registry limits: 2 GB per compressed layer, 15 GB total image size, 4 MB manifest, 1 MB config blob. Layers must be gzip or zstd compressed — uncompressed OCI layers are rejected. Repositories per project: 10 (Hobby) / 1,000 (Pro) / 5,000 (Enterprise). Storage is billed at $0.10 per GB.

### When to reach for a container

Good fits: Go, Rust, Ruby, PHP, or other backends; apps needing system libraries like FFmpeg or Chromium; frameworks outside Vercel's auto-detection; guaranteed build/runtime parity across environments.

Poor fits: anything that must hold state in-process, keep a daemon alive between requests, or run background work independent of a request. Reach for Workflow, Queues, or Cron for those.

If your framework is already auto-detected and you have no system-library needs, the standard build is simpler and faster — a Dockerfile is not an upgrade by default.

## Plan Limits at a Glance

| | Hobby | Pro | Enterprise |
|---|---|---|---|
| Duration (default / max) | 300s / **300s** | 300s / 800s | 300s / 800s |
| Extended duration (beta) | — | 1800s | 1800s |
| Memory / CPU | 2 GB / 1 vCPU, not configurable | Standard or Performance (4 GB / 2 vCPU) | Standard or Performance |
| Bundle size | 250 MB (500 MB Python), 5 GB with large functions beta | same | same |
| Concurrency | auto-scales to 30,000 | 30,000 | 100,000+ |
| Regions | single region | up to 3 | all |
| Edge code size (gzipped) | 1 MB | 2 MB | 4 MB |
| VCR repos per project | 10 | 1,000 | 5,000 |
| Request/response body | 4.5 MB | 4.5 MB | 4.5 MB |

### What changed for Hobby

Hobby function limits went **up substantially** with Fluid Compute, and stale 10s/60s numbers are a common source of bad advice:

- **Duration: 60s → 300s for both the default and the maximum** — a 5× increase. Hobby functions can run a full five minutes.
- **CPU: the Basic instance was removed; Hobby now runs Standard**, 1 vCPU / 2 GB (up from 1 vCPU / 1.7 GB), managed by Vercel with a minimum of 1 vCPU.
- Hobby still cannot configure memory/CPU, use the extended 30-minute duration, or run in multiple regions — those remain Pro/Enterprise.

## Streaming

Zero-config streaming on the default Node.js runtime, including Server-Sent Events (SSE). Essential for AI applications.

> **You do NOT need `runtime = 'edge'` for streaming or SSE.** Streaming responses (`ReadableStream`, `text/event-stream`) work on the default Node.js runtime — this is the single most common reason people wrongly reach for Edge. Stay on Node.js (Fluid Compute) so you keep full Node.js APIs, npm packages, and longer durations; Edge offers no streaming advantage and caps you at 25s to first byte.

```ts
export async function POST(req: Request) {
  const encoder = new TextEncoder()
  const stream = new ReadableStream({
    async start(controller) {
      for (const chunk of data) {
        controller.enqueue(encoder.encode(chunk))
        await new Promise(r => setTimeout(r, 100))
      }
      controller.close()
    },
  })

  return new Response(stream, {
    headers: { 'Content-Type': 'text/event-stream' },
  })
}
```

For AI streaming, use the AI SDK's `toUIMessageStreamResponse()` (for chat UIs with `useChat`) which handles SSE formatting automatically.

## WebSockets

Vercel Functions can hold open bidirectional WebSocket connections — use them for realtime features like interactive AI streaming, chat, and collaborative apps. There is **no separate WebSocket-server product and no third-party service (Pusher, Ably, etc.) required** — it runs on Vercel Functions directly. Requires **Fluid Compute**, which is the default for new projects.

**How it works**: a WebSocket starts as an HTTP `GET` with an `Upgrade` header, so it passes through the same Routing Middleware, rewrites, Firewall rules, and rate limits as any other request. After the upgrade, the connection is pinned to a single function instance for its lifetime; Fluid Compute lets one instance serve many concurrent connections. Active CPU pricing means you're billed while processing messages, not for idle open connections — the same limits and pricing as other Function invocations apply.

### `ws` (no extra config)

WebSockets work like any distributed WebSocket server — export an `http.Server` and use a library such as `ws`:

```ts
// api/ws.ts
import http from 'http'
import { WebSocketServer } from 'ws'

const server = http.createServer()
const wss = new WebSocketServer({ server })

wss.on('connection', (ws) => {
  ws.on('message', (data) => ws.send(data)) // echo
})

export default server
```

### Socket.IO

Higher-level realtime libraries like Socket.IO work too. Configure the **client** to use the WebSocket transport directly — Socket.IO defaults to HTTP long-polling, which won't work:

```ts
// api/socket-io.ts
import http from 'http'
import { Server } from 'socket.io'

const server = http.createServer()
const io = new Server(server)

io.on('connection', (socket) => {
  socket.on('message', (data) => socket.send(data))
})

export default server
```

```ts
// client.ts
import { io } from 'socket.io-client'

const socket = io('https://your-domain.com', {
  // Socket.IO appends /socket.io, so the full path becomes /api/socket-io/socket.io
  path: '/api/socket-io/socket.io',
  transports: ['websocket'], // required — Socket.IO defaults to HTTP long-polling
})
```

Express, Hono, and Nitro (including Nuxt, via native WebSocket support) serve WebSockets the same way — export the HTTP server. Python frameworks work too: FastAPI handles the upgrade natively, and `python-socketio` is protocol-compatible with the JS Socket.IO client.

### Next.js

Next.js doesn't expose an API for handling WebSocket upgrades. Use `experimental_upgradeWebSocket()` from `@vercel/functions` inside a route handler:

```ts
// app/api/ws/route.ts
import { experimental_upgradeWebSocket, type WebSocketData } from '@vercel/functions'

export async function GET() {
  return experimental_upgradeWebSocket((ws) => {
    ws.on('message', (data: WebSocketData) => ws.send(data))
  })
}
```

### Reconnects and persistent state

- **Connections close when the function reaches its max duration.** Clients must reconnect with backoff, then resubscribe to channels and reload any state they need.
- **No instance affinity across connections.** A reconnect — or a new deployment — may land on a different instance, so never keep durable state, presence, rooms, or pub/sub coordination in memory. Use an external store such as [Redis from the Marketplace](https://vercel.com/marketplace/redis).

```ts
// client.ts — reconnect with exponential backoff
let socket: WebSocket
let delay = 1000

function connect() {
  socket = new WebSocket('wss://your-domain.com/api/ws')
  socket.addEventListener('open', () => { delay = 1000 })
  socket.addEventListener('message', (e) => console.log(e.data))
  socket.addEventListener('close', () => {
    setTimeout(connect, delay)
    delay = Math.min(delay * 2, 30000)
  })
}

connect()
```

## Cron Jobs

Schedule function invocations via `vercel.json`:

```json filename="vercel.json"
{
  "crons": [
    {
      "path": "/api/daily-report",
      "schedule": "0 8 * * *"
    },
    {
      "path": "/api/cleanup",
      "schedule": "0 */6 * * *"
    }
  ]
}
```

The cron endpoint receives a normal HTTP request. Verify it's from Vercel:

```ts
export async function GET(req: Request) {
  const authHeader = req.headers.get('authorization')
  if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) {
    return new Response('Unauthorized', { status: 401 })
  }
  // Do scheduled work
  return Response.json({ ok: true })
}
```

## Configuration

`vercel.ts` is the recommended way to configure a project — full TypeScript types, dynamic logic, and env access via `@vercel/config`. `vercel.json` remains fully supported. Legacy `now.json` support ended **March 31, 2026**; rename it to `vercel.json` (no content changes required).

```ts
// vercel.ts
import type { VercelConfig } from '@vercel/config/v1'

export const config: VercelConfig = {
  functions: {
    'app/api/heavy/**': { maxDuration: 800 },
    'app/api/report/**': { maxDuration: 1800 }, // Pro/Ent extended-duration beta
  },
  crons: [{ path: '/api/cleanup', schedule: '0 0 * * *' }],
}
```

The `vercel.json` equivalent:

```json
{
  "$schema": "https://openapi.vercel.sh/vercel.json",
  "functions": {
    "app/api/heavy/**": { "maxDuration": 800 },
    "api/upload.js": { "supportsCancellation": true }
  }
}
```

What you **cannot** put here:
- `memory` — with Fluid Compute (the default), set it in the dashboard; Pro/Enterprise only, and `vercel.json` warns at build time
- A project-wide default above 800s — extended durations are per-function only

`runtime: "edge"` is accepted here, but prefer leaving it out — see [Prefer Node.js over the Edge runtime](#prefer-nodejs-over-the-edge-runtime).

## Common Pitfalls

1. **`waitUntil` given a callback**: it takes a Promise. `waitUntil(fn())`, never `waitUntil(fn)` or `waitUntil(async () => {})` — the latter silently does nothing
2. **Cold starts with DB connections**: use connection pooling (e.g. Neon's `@neondatabase/serverless`)
3. **Reaching for the Edge runtime**: prefer Node.js — see [Prefer Node.js over the Edge runtime](#prefer-nodejs-over-the-edge-runtime)
4. **Timeout exceeded**: raise `maxDuration` (800s Pro/Ent, 1800s in beta), or move to Workflow for anything longer
5. **Bundle size**: standard limit is 250 MB uncompressed (500 MB Python). 5 GB needs the large functions beta, which existing projects must opt into with `VERCEL_SUPPORT_LARGE_FUNCTIONS=1`
6. **Payload size**: request and response bodies cap at **4.5 MB** (`413 FUNCTION_PAYLOAD_TOO_LARGE`) — use Blob client uploads or streaming, not a bigger function
7. **In-memory state**: Fluid shares instances across invocations and scales to zero — never keep sessions, rooms, or caches in process memory
8. **Setting `memory` in `vercel.json`**: with Fluid Compute enabled this is not the place for it and the build warns — set it in the dashboard
9. **Environment variables**: available in all functions automatically; use `vercel env pull` for local dev

## Function Runtime Diagnostics

### Timeout Diagnostics

```
504 FUNCTION_INVOCATION_TIMEOUT?
├─ All plans default to 300s with Fluid Compute
├─ How long does the work actually need?
│  ├─ ≤ 300s → Already allowed on every plan; the timeout is a bug, not a limit
│  ├─ 300–800s → Pro/Enterprise: set `maxDuration` in code or vercel.json
│  ├─ 800–1800s → Pro/Enterprise extended-duration beta (30 min)
│  │   ├─ Must be set PER FUNCTION — project defaults above 800s are ignored
│  │   ├─ Runtimes: nodejs20/22/24.x, Bun 1.x/1.4.x, python3.12/3.13/3.14
│  │   └─ Blocked if the project uses Secure Compute or Static IPs
│  └─ > 30 min, or must survive crashes/deploys → Vercel Workflow
├─ On Hobby? → 300s is both default AND max; no extension exists, upgrade to Pro
├─ Client disconnected before the function finished?
│  └─ HTTP/1.1 drops idle connections → stream heartbeat/progress data
└─ DB query slow? → Add connection pooling, check cold start, use Global Config
```

### 500 Error Diagnostics

```
500 Internal Server Error?
├─ Check Vercel Runtime Logs (Dashboard → Deployments → Functions tab)
├─ Missing env vars? → Compare `.env.local` against Vercel dashboard settings
├─ Import error? → Verify package is in `dependencies`, not `devDependencies`
└─ Uncaught exception? → Wrap handler in try/catch, use `after()` for error reporting
```

### Invocation Failure Diagnostics

```
"FUNCTION_INVOCATION_FAILED"?
├─ Memory exceeded (OOM)?
│  ├─ Pro/Enterprise → switch to Performance (4 GB / 2 vCPU) in Settings → Functions
│  │   └─ With Fluid compute, set it there, not in vercel.json (which warns at build)
│  └─ Hobby → fixed at 2 GB / 1 vCPU; reduce per-request memory or upgrade
├─ Crashed during init? → Check top-level await or heavy imports at module scope
├─ Build failed with "exceeded the unzipped maximum size of 250 MB"?
│  ├─ Trim with excludeFiles / outputFileTracingExcludes first
│  └─ Then large functions beta: VERCEL_SUPPORT_LARGE_FUNCTIONS=1 (5 GB, Node/Bun/Python)
├─ 413 FUNCTION_PAYLOAD_TOO_LARGE? → 4.5 MB body cap; use Blob client uploads or streaming
└─ Container image? → Is it listening on port 80 (or $PORT)? Is it holding state between requests?
```

### Cold Start Diagnostics

```
Cold start latency > 1s?
├─ Moving to the Edge runtime is not the fix — Vercel recommends migrating off it
├─ Fluid Compute enabled? → Reuses warm instances across concurrent invocations
├─ Measuring in preview? → Bytecode caching is production-only; re-measure in prod
├─ Large function bundle? → Audit imports, use dynamic imports, tree-shake
├─ DB connection in cold start? → Use connection pooling (Neon serverless driver)
└─ Container image? → Scales to zero after 5 min idle (30 s in preview); expect cold starts
```

### Edge Function Timeout Diagnostics

```
"EDGE_FUNCTION_INVOCATION_TIMEOUT"?
├─ Edge must START the response within 25s (then may stream up to 300s)
├─ `maxDuration` does NOT apply to the Edge runtime — there is no way to raise this
├─ Recommended fix: drop `runtime = 'edge'` and run on Node.js
│  └─ Node.js gives you 300s by default, 800s on Pro/Ent, 1800s in the beta
└─ On Next.js 16.3+, `runtime = 'edge'` is unsupported — migration is required there
```

## Official Documentation

- [Vercel Functions](https://vercel.com/docs/functions)
- [Functions limits](https://vercel.com/docs/functions/limitations) — duration, memory, bundle size, large functions
- [Configuring max duration](https://vercel.com/docs/functions/configuring-functions/duration) — including the extended 30-minute beta
- [Configuring memory / CPU](https://vercel.com/docs/functions/configuring-functions/memory)
- [Functions API reference](https://vercel.com/docs/functions/functions-api-reference) — `waitUntil`, `getDeadline`, SIGTERM, cancellation
- [Fluid Compute](https://vercel.com/docs/fluid-compute)
- [Container Images](https://vercel.com/docs/functions/container-images) — Dockerfile on Vercel
- [Vercel Container Registry](https://vercel.com/docs/container-registry) and its [limits and pricing](https://vercel.com/docs/container-registry/limits-and-pricing)
- [Services](https://vercel.com/docs/services) — multiple backends/frontends in one project
- [Streaming](https://vercel.com/docs/functions/streaming-functions)
- [WebSockets](https://vercel.com/docs/functions/websockets)
- [Cron Jobs](https://vercel.com/docs/cron-jobs)
- [Vercel Workflow](https://vercel.com/docs/workflows) — for anything beyond 30 minutes
- [Edge Runtime](https://vercel.com/docs/functions/runtimes/edge) — legacy; Vercel recommends migrating to Node.js
- [GitHub: Vercel](https://github.com/vercel/vercel)
