neon-functions
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseFIRST: Use the parent skill for a Neon overview, getting started with Neon, Neon development best practices, and more.
neonIf the skill is not installed, fetch it from https://neon.com/docs/ai/skills/neon/SKILL.md or install it with:
neonbash
npx skills add neondatabase/agent-skills --skill neon首先:使用父级技能获取Neon概览、快速入门、Neon开发最佳实践等内容。
neon如果未安装技能,可以从https://neon.com/docs/ai/skills/neon/SKILL.md获取,或者通过以下命令安装:
neonbash
npx skills add neondatabase/agent-skills --skill neonNeon Functions
Neon Functions
This is a public beta feature and only available in .
us-east-2Neon Functions are long-running Node.js HTTP handlers deployed onto a Neon branch. Each function gets a public HTTPS URL, runs in the same region as your database, and — if the branch has Postgres — gets injected automatically. You deploy and manage them through the same Neon CLI, , and API you already use.
DATABASE_URLneon.tsUse this skill to help the user define, run locally, deploy, and manage functions next to their database. Deliver a deployed function with its invocation URL, a working local loop, or a precise answer from the official Neon docs.
neon dev这是一项公开测试版功能,仅在区域可用。
us-east-2Neon Functions是部署在Neon分支上的长期运行Node.js HTTP处理程序。每个函数都有一个公开的HTTPS URL,与您的数据库在同一区域运行——如果分支包含Postgres,会自动注入。您可以通过已熟悉的Neon CLI、和API来部署和管理这些函数。
DATABASE_URLneon.ts使用本技能帮助用户定义、本地运行、部署和管理数据库旁的函数。提供已部署函数的调用URL、可用的本地循环,或来自Neon官方文档的精准答案。
neon devWhen to Use
适用场景
Reach for Neon Functions when the workload is a request/response handler that benefits from staying alive and staying close to the data:
- Long-running request/response flows that outlast lambda-style limits. Agents that make several LLM calls and tool invocations per request, or image/video generation, routinely blow past the ~10–60s execution caps and short streaming windows of traditional serverless functions. Neon Functions are long-running: the handler just needs to start responding within 15 minutes, and an open stream stays alive as long as bytes keep flowing. That's enough headroom for real agent workloads.
- Stateful streaming without bolting on Redis. Because a function stays alive across a request, it can host an SSE endpoint or a WebSocket server and hold the connection open in-process — no external state store (Redis, etc.) needed just to keep a stream coherent. Module-scope state (a pool, an in-memory counter) persists across requests on the same isolate.
pg - Compute that must sit next to Postgres. The function runs in the same region as the branch's database, so there are no cross-region round trips on every query. is injected for you.
DATABASE_URL - A backend that branches with your data. Each branch runs its own version of the function at its own URL, against its own isolated database (and storage, and gateway) state. Preview deployments, CI, and dev environments each get a self-contained backend — deploying to a child never affects the parent.
- Webhooks, bots, and post-response work. Webhook handlers that fan out into multiple DB writes, Discord/WebSocket bots, and fire-and-forget follow-ups via (analytics, audit logs) all fit.
waitUntil
If the workload is a pure static site, a cron/background job that needs its own lifecycle and cancellation, or something that must run outside today, this isn't the right tool yet (see Timeouts and Runtime Limits and Availability).
us-east-2当工作负载为请求/响应处理程序,且受益于持续运行和贴近数据时,可选择Neon Functions:
- 超出Lambda风格限制的长期请求/响应流程:每个请求中进行多次LLM调用和工具调用的Agent,或图像/视频生成任务,通常会突破传统无服务器函数约10-60秒的执行上限和短流式窗口。Neon Functions支持长期运行:处理程序只需在15分钟内开始响应,且只要有字节流动,打开的流就会保持活跃。这为实际Agent工作负载提供了足够的空间。
- 无需额外集成Redis的有状态流式处理:由于函数在请求期间保持活跃,它可以托管SSE端点或WebSocket服务器,并在进程内保持连接打开——无需外部状态存储(如Redis等)即可保持流的连贯性。模块级状态(如连接池、内存计数器)在同一隔离实例的多个请求间持久化。
pg - 必须紧邻Postgres的计算资源:函数与分支数据库在同一区域运行,因此每次查询不会产生跨区域往返延迟。会自动注入。
DATABASE_URL - 随数据分支同步的后端:每个分支在自己的URL上运行函数的独立版本,对应自己的隔离数据库(以及存储、网关)状态。预览部署、CI和开发环境各自拥有独立的后端——部署到子分支永远不会影响父分支。
- Webhook、机器人和响应后任务:需要扩散到多个数据库写入的Webhook处理程序、Discord/WebSocket机器人,以及通过执行的即发即弃后续任务(如分析、审计日志)都适用。
waitUntil
What It Does
功能特性
- Long-running & serverless — Built for WebSocket servers (see WebSocket Servers), SSE endpoints (see Server-Sent Events (SSE)), long agent HTTP streams, and APIs. Still scales to zero when idle.
- Web-standard handler — A function is any default export with a method returning a
fetch(request)(Workers/WinterTC-compatible). A Hono app exports exactly that shape, soResponsejust works. Runs on Node.js 24, so all Node APIs are available.export default app - Close to your database — Runs in the branch's region; injected automatically when the branch has Postgres.
DATABASE_URL - Branchable — Each branch runs its own function version at its own URL against its own isolated state.
- Same CLI/API — Deploy and manage via ,
neon, or the Neon API.neon.ts
- 长期运行且无服务器 —— 专为WebSocket服务器(请查看WebSocket服务器)、SSE端点(请查看服务器发送事件(SSE))、长期Agent HTTP流和API构建。空闲时仍可缩容至零。
- 符合Web标准的处理程序 —— 函数是任何默认导出的、带有方法并返回
fetch(request)的对象(兼容Workers/WinterTC)。Hono应用正好符合此结构,因此Response可直接运行。基于Node.js 24构建,支持所有Node API。export default app - 贴近数据库 —— 在分支所在区域运行;当分支包含Postgres时自动注入。
DATABASE_URL - 可分支同步 —— 每个分支在自己的URL上运行独立的函数版本,对应自己的隔离状态。
- 统一CLI/API —— 通过、
neon或Neon API进行部署和管理。neon.ts
Availability
可用性
Check this precondition before setting anything up: Neon Functions is a public beta feature available only on new projects in the region. Confirm the user's Neon project is a new project in ; it can't be enabled on existing projects. Functions usage isn't billed during the public beta.
us-east-2us-east-2在设置前请检查以下前提条件:Neon Functions是公开测试版功能,仅在区域的新项目中可用。确认用户的Neon项目是区域的新项目;现有项目无法启用该功能。公开测试期间,Functions使用不收取费用。
us-east-2us-east-2Architecture: Where Functions Fit
架构:Functions的定位
Neon (Functions included) is backend primitives, not full-stack app hosting. Host your app on Vercel (or Netlify, or another frontend/app host); Functions are the long-running, stateful slice of your backend that lives next to your data. They compose with that platform in two ways:
- Add a Function to a full-stack app. Your Next.js / TanStack Start app on Vercel (or Netlify) owns UI, auth (e.g. Neon Auth), and talks directly to Lakebase Postgres and Object Storage. When one workload outgrows the host's short serverless limits — a WebSocket or SSE server, or a long-running agent that would time out — move just that piece onto a Neon Function. (See Functions as an Agent Backend for the client-direct pattern.)
- Run the whole backend control plane on Functions. Especially when the frontend is client-only — TanStack Router, React Router in client mode, and similar SPAs hosted on Vercel or Netlify — the client calls Functions directly. Build REST APIs and request/response agents, host MCP servers, and run anything stateful or that belongs close to Postgres and Object Storage.
Either way, secure a Function like any standalone REST API: verify a JWT or API key at the top of the handler (see the WARNING under Functions as an Agent Backend). Because a Function is just your backend, you can move pieces between your host and Neon — relocate an agent or a stateful WebSocket server onto a Function when it needs more runtime, and back if needed.
Neon(包含Functions)是后端原语,而非全栈应用托管平台。将您的应用托管在Vercel(或Netlify等前端/应用托管平台);Functions是您后端中贴近数据的长期运行、有状态部分。它与该平台有两种组合方式:
- 为全栈应用添加Function:您在Vercel(或Netlify)上的Next.js/TanStack Start应用负责UI、认证(如Neon Auth),并直接与Lakebase Postgres和对象存储交互。当某一工作负载超出托管平台的短时长无服务器限制(如WebSocket或SSE服务器,或会超时的长期Agent),只需将该部分迁移到Neon Function。(请查看作为Agent后端的Functions了解客户端直接调用模式。)
- 在Functions上运行整个后端控制平面:尤其是当前端为纯客户端(如TanStack Router、客户端模式的React Router等托管在Vercel或Netlify上的SPA)时,客户端可直接调用Functions。构建REST API和请求/响应Agent,托管MCP服务器,运行任何有状态或需贴近Postgres和对象存储的任务。
无论哪种方式,都要像保护独立REST API一样保护Function:在处理程序顶部验证JWT或API密钥(请查看作为Agent后端的Functions下的警告)。由于Function就是您的后端,您可以在托管平台和Neon之间迁移组件——当Agent或有状态WebSocket服务器需要更长运行时间时,将其迁移到Function,必要时再迁回。
Setup
设置
Functions are declared in (see the skill for the branch-first workflow and basics). Add and declare functions under , keyed by slug:
neon.tsneonneon.ts@neon/configpreview.functionstypescript
// neon.ts
import { defineConfig } from "@neon/config/v1";
export default defineConfig({
preview: {
functions: {
todos: {
// slug: ^[a-z0-9]{1,20}$ — lowercase letters/digits, no hyphens
name: "todo api", // display label only
source: "src/index.ts", // entry file, relative to neon.ts
},
},
},
});The slug is the function's permanent identity (it appears in the invocation URL and CLI commands) and can't be changed after the first deploy. Use for a human-readable label.
nameA minimal function — a Hono app that queries the branch's Postgres via the injected :
DATABASE_URLtypescript
// src/index.ts
import { Hono } from "hono";
import { drizzle } from "drizzle-orm/node-postgres";
import { Pool } from "pg";
import { parseEnv } from "@neon/env";
import config from "../neon";
import { todos } from "./db/schema";
const env = parseEnv(config);
const pool = new Pool({ connectionString: env.postgres.databaseUrl, max: 5 });
const db = drizzle(pool);
const app = new Hono();
app.get("/", (c) => c.text("Neon + Hono + Drizzle"));
app.post("/todos", async (c) => {
const { text } = await c.req.json<{ text: string }>();
const [row] = await db.insert(todos).values({ text }).returning();
return c.json(row, 201);
});
app.get("/todos", async (c) => c.json(await db.select().from(todos)));
export default app;Create the pool at module scope (reused across requests on the same isolate) and keep small (e.g. 5), since each isolate keeps its own pool.
pgmaxparseEnv(config)parseEnvneon.tstypescript
const { postgres } = parseEnv(config, ["DATABASE_URL"]); // not the unpooled URL, auth, etc.
const pool = new Pool({ connectionString: postgres.databaseUrl, max: 5 });Functions在中声明(请查看技能了解分支优先工作流和基础知识)。添加并在下声明函数,以slug作为键:
neon.tsneonneon.ts@neon/configpreview.functionstypescript
// neon.ts
import { defineConfig } from "@neon/config/v1";
export default defineConfig({
preview: {
functions: {
todos: {
// slug格式:^[a-z0-9]{1,20}$ —— 小写字母/数字,无连字符
name: "todo api", // 仅为显示标签
source: "src/index.ts", // 相对于neon.ts的入口文件
},
},
},
});slug是函数的永久标识(出现在调用URL和CLI命令中),首次部署后无法更改。使用设置人类可读的标签。
name一个极简函数——通过注入的查询分支Postgres的Hono应用:
DATABASE_URLtypescript
// src/index.ts
import { Hono } from "hono";
import { drizzle } from "drizzle-orm/node-postgres";
import { Pool } from "pg";
import { parseEnv } from "@neon/env";
import config from "../neon";
import { todos } from "./db/schema";
const env = parseEnv(config);
const pool = new Pool({ connectionString: env.postgres.databaseUrl, max: 5 });
const db = drizzle(pool);
const app = new Hono();
app.get("/", (c) => c.text("Neon + Hono + Drizzle"));
app.post("/todos", async (c) => {
const { text } = await c.req.json<{ text: string }>();
const [row] = await db.insert(todos).values({ text }).returning();
return c.json(row, 201);
});
app.get("/todos", async (c) => c.json(await db.select().from(todos)));
export default app;在模块级创建连接池(同一隔离实例的多个请求间复用),并将设置为较小值(如5),因为每个隔离实例都有自己的连接池。
pgmaxparseEnv(config)parseEnvneon.tstypescript
const { postgres } = parseEnv(config, ["DATABASE_URL"]); // 非非池化URL、认证信息等
const pool = new Pool({ connectionString: postgres.databaseUrl, max: 5 });Develop Locally and Deploy
本地开发与部署
bash
neon dev # serves every function in neon.ts with hot reload; injects DATABASE_URL & friends
neon deploy # bundles with esbuild, uploads, and applies neon.ts to the linked branchTo deploy a single function without : ( takes either the entry file or a directory containing , , or ). Retrieve the public URL with (the field, of the form ). Manage with .
neon.tsneon functions deploy <slug> --src src/index.ts--srcindex.tsindex.mjsindex.jsneon functions get <slug>invocation_urlhttps://<branch_id>-<slug>.compute.c-1.us-east-2.aws.neon.techneon functions list|get|deleteWhen creates a new branch and a is present, it applies the policy automatically — deploying the function to the fresh branch. Checking out an existing branch does not re-deploy; run explicitly.
neon checkoutneon.tsneon deploybash
neon dev # 热重载运行neon.ts中的所有函数;注入DATABASE_URL等环境变量
neon deploy # 使用esbuild打包、上传,并将neon.ts应用到关联分支无需即可部署单个函数:(接受入口文件或包含、或的目录)。使用获取公开URL(字段,格式为)。使用进行管理。
neon.tsneon functions deploy <slug> --src src/index.ts--srcindex.tsindex.mjsindex.jsneon functions get <slug>invocation_urlhttps://<branch_id>-<slug>.compute.c-1.us-east-2.aws.neon.techneon functions list|get|delete当创建新分支且存在时,会自动应用策略——将函数部署到新分支。检出现有分支不会重新部署;需显式运行。
neon checkoutneon.tsneon deployNeon Infrastructure as Code (neon.ts
)
neon.tsNeon基础设施即代码(neon.ts
)
neon.tsThe block from Setup is part of , Neon's infrastructure-as-code file — one TypeScript file declares every function (its , display , and ) alongside any other branch services, in version control (see the skill for the full reference). Treat it like Terraform for your branch:
preview.functionsneon.tssourcenameenvneonbash
neon config status # print the branch's live config (deployed functions)
neon config plan # dry-run diff of what apply would change
neon config apply # bundle + deploy the declared functions (neon deploy is an alias)Functions are branch-scoped: each branch runs its own deployment at its own URL. When a is present, applies the policy as it creates a branch, so a fresh preview/CI branch comes up with the function already deployed. Checking out an existing branch doesn't redeploy — run to apply changes.
neon.tsneon checkoutneon deployPer-branch deploy tuning (e.g. ) lives in the closure, keyed by slug, so it can vary by branch without changing which functions exist:
runtimebranchtypescript
export default defineConfig({
preview: {
functions: { todos: { name: "todo api", source: "src/index.ts" } },
},
branch: (branch) => ({
preview: { functions: { todos: { runtime: "nodejs24" } } },
}),
});设置中的块是的一部分,是Neon的基础设施即代码文件——一个TypeScript文件声明所有函数(其、显示和)以及任何其他分支服务,并纳入版本控制(请查看技能获取完整参考)。将其视为分支的Terraform:
preview.functionsneon.tsneon.tssourcenameenvneonbash
neon config status # 打印分支的实时配置(已部署的函数)
neon config plan # 试运行apply操作的差异
neon config apply # 打包并部署声明的函数(neon deploy是别名)Functions是分支范围的:每个分支在自己的URL上运行独立的部署版本。当存在时,在创建分支时应用策略,因此新的预览/CI分支创建后会自动部署函数。检出现有分支不会重新部署——运行以应用更改。
neon.tsneon checkoutneon deploy每个分支的部署调优(如)位于闭包中,以slug为键,因此无需更改函数即可在不同分支间调整:
runtimebranchtypescript
export default defineConfig({
preview: {
functions: { todos: { name: "todo api", source: "src/index.ts" } },
},
branch: (branch) => ({
preview: { functions: { todos: { runtime: "nodejs24" } } },
}),
});Environment Variables
环境变量
Neon injects branch-scoped connection strings and service URLs at runtime — you don't declare these or pass them at deploy time:
| Variable | Notes |
|---|---|
| The branch name (e.g. |
| Pooled connection string. Use for most queries. Present only if the branch has Postgres. |
| Direct connection. Use for migrations, |
| Present when Neon Auth is enabled on the branch. |
| Present when the Data API is enabled on the branch. |
Object storage () and AI Gateway () vars are also injected when those services are declared — see the and skills.
AWS_*NEON_AI_GATEWAY_*neon-object-storageneon-ai-gatewayneon env pullneon-env runneon devNEON_BRANCHYour own secrets are per-deployment. Set them with on (repeatable; deletes a key, unmentioned keys carry over), or declare them in under the function's (resolved at deploy time, so read from to avoid hardcoding):
--env KEY=VALUEneon functions deploy--env KEY=neon.tsenvprocess.envtypescript
functions: {
todos: {
name: "todo api",
source: "src/index.ts",
env: { RESEND_API_KEY: process.env.RESEND_API_KEY! },
},
}Load a before deploy with . Pull the branch's Neon-managed vars onto disk for local dev with (/ do this automatically; pass to skip and use for runtime injection). Limits: ≤1,000 vars, ≤64 KiB total, and the prefix is reserved.
.envneon deploy --env .env.productionneon env pulllinkcheckout--no-env-pullneon-env run -- <cmd>NEON_Neon在运行时注入分支范围的连接字符串和服务URL——您无需在部署时声明或传递这些变量:
| 变量名 | 说明 |
|---|---|
| 分支名称(如 |
| 池化连接字符串。适用于大多数查询。仅当分支包含Postgres时存在。 |
| 直接连接字符串。适用于迁移、 |
| 当分支启用Neon Auth时存在。 |
| 当分支启用Data API时存在。 |
当声明对象存储()和AI网关()服务时,也会注入相应变量——请查看和技能。
AWS_*NEON_AI_GATEWAY_*neon-object-storageneon-ai-gatewayneon env pullneon-env runneon devNEON_BRANCH您自己的密钥是每个部署独立的。可在时使用设置(可重复使用;删除密钥,未提及的密钥会保留),或在中函数的下声明(部署时解析,因此从读取以避免硬编码):
neon functions deploy--env KEY=VALUE--env KEY=neon.tsenvprocess.envtypescript
functions: {
todos: {
name: "todo api",
source: "src/index.ts",
env: { RESEND_API_KEY: process.env.RESEND_API_KEY! },
},
}部署前使用加载文件。使用将分支的Neon托管变量拉取到本地用于开发(/会自动执行此操作;传递可跳过,使用进行运行时注入)。限制:最多1000个变量,总大小不超过64 KiB,前缀为保留前缀。
neon deploy --env .env.production.envneon env pulllinkcheckout--no-env-pullneon-env run -- <cmd>NEON_Connecting to Postgres
连接Postgres
When the branch has Postgres, Neon injects the connection strings at runtime — you don't declare them, pass them at deploy time, or hardcode anything. The two you'll use:
- — pooled connection string (routed through Neon's connection pooler). Use it for normal request/response query traffic. Kept un-prefixed because every Postgres ORM (Drizzle, Prisma, Knex, …) reads
DATABASE_URLby default.DATABASE_URL - — direct connection string to the same database. Use it for migrations,
DATABASE_URL_UNPOOLED/LISTEN, and long multi-statement transactions.NOTIFY
Use Drizzle (or another ORM) on top of node-postgres () for queries and schema management — not Neon's serverless driver. Functions are long-running and reuse an isolate across many requests, so a persistent pool is the right fit; the serverless driver's HTTP transport is meant for fully isolated, lambda-style runtimes.
pgpgCreate the connection pool once at module scope and reuse it across requests — don't open a connection per request:
typescript
import { drizzle } from "drizzle-orm/node-postgres";
import { Pool } from "pg";
// Created once per isolate; reused by every request that isolate handles.
const pool = new Pool({ connectionString: process.env.DATABASE_URL, max: 5 });
const db = drizzle(pool);Pooling is recommended because an isolate is reused across many requests (and several requests can be in flight on the same isolate at once — see Timeouts and Runtime Limits). A module-scope pool is opened once on cold start and then shared by every subsequent request that isolate serves, so you amortize connection setup instead of paying it on every request and you avoid exhausting Postgres connections under load.
Keep small (e.g. ): each isolate keeps its own pool, so total connections to Postgres scale with the number of live isolates. You don't need to close the pool on shutdown — when the runtime evicts an isolate it sends /, and Neon's pooler reclaims those connections for you, so an explicit drain handler is redundant.
max5SIGINTSIGTERMReadingdirectly works everywhere. The function in Setup instead usesprocess.env.DATABASE_URL's@neon/envto read the same value in a typed, validated way — either is fine.parseEnv(config)
当分支包含Postgres时,Neon会在运行时自动注入连接字符串——您无需声明、在部署时传递或硬编码任何内容。常用的两个字符串:
- —— 池化连接字符串(通过Neon的连接池路由)。适用于常规请求/响应查询流量。保持无前缀是因为所有Postgres ORM(Drizzle、Prisma、Knex等)默认读取
DATABASE_URL。DATABASE_URL - —— 直接连接到同一数据库的字符串。适用于迁移、
DATABASE_URL_UNPOOLED/LISTEN和长多语句事务。NOTIFY
**在node-postgres()之上使用Drizzle(或其他ORM)**进行查询和架构管理——不要使用Neon的无服务器驱动。Functions是长期运行的,会在多个请求间复用隔离实例,因此持久化的连接池是合适的选择;无服务器驱动的HTTP传输专为完全隔离的Lambda风格运行时设计。
pgpg在模块级创建一次连接池并在多个请求间复用——不要为每个请求打开连接:
typescript
import { drizzle } from "drizzle-orm/node-postgres";
import { Pool } from "pg";
// 每个隔离实例创建一次;由该隔离实例处理的所有请求复用。
const pool = new Pool({ connectionString: process.env.DATABASE_URL, max: 5 });
const db = drizzle(pool);推荐使用连接池,因为隔离实例会在多个请求间复用(同一隔离实例上可能同时有多个请求在处理——请查看超时和运行时限制)。模块级连接池在冷启动时创建一次,然后由该隔离实例服务的所有后续请求共享,因此您可以分摊连接设置成本,而不是为每个请求付出成本,并且避免在负载下耗尽Postgres连接。
将设置为较小值(如):每个隔离实例都有自己的连接池,因此Postgres的总连接数随活跃隔离实例的数量扩展。您无需在关闭时关闭连接池——当运行时驱逐隔离实例时会发送/,Neon的池化器会为您回收这些连接,因此显式的 drain 处理程序是多余的。
max5SIGINTSIGTERMTimeouts and Runtime Limits
超时和运行时限制
Functions are long-running but still serverless — they are a request/response runtime, not a background job runner. The hard limits:
- Time to first byte: 15 minutes. Your handler must begin returning a response within 15 minutes of receiving a request. Most handlers finish in seconds; the 15-minute ceiling exists so agent workloads like image/video generation have room.
- Heartbeat: 15 minutes. Open WebSocket/SSE connections stay alive as long as data flows. The timeout only fires when a connection goes silent — send at least one byte every 15 minutes to keep a quiet stream alive.
- : 15 minutes. Work registered with
waitUntilkeeps the invocation alive after the response is sent, up to 15 minutes — for cleanup like analytics writes and audit logs, not a background job runner. (waitUntilfromwaitUntilis currently a stub during the preview.)@neon/functions - Idle eviction. With no active connections Neon shuts the function down; it may also evict/restart for operational reasons — e.g. maintenance, or moving the function to a different compute node (active functions can run for hours first). Treat eviction like a process restart — WebSocket/SSE clients must reconnect. Neon sends before evicting, so a
SIGINThandler lets you detect that the function is about to be evicted and run any last-minute cleanup. You don't need one just to close Postgres connections — Neon's pooler reclaims those on its own.process.on("SIGINT", ...) - Runtime: Node.js 24, memory fixed at 2048 MiB during the preview. Slugs must match . An isolate is reused across many requests — multiple requests can be in flight on the same isolate at once (interleaved on Node's single-threaded event loop), and under load the runtime runs several isolates in parallel, each with its own copy of module state. State held in module scope is therefore per-isolate (shared by every request that isolate handles) and in-memory only — persist anything that must survive eviction in Postgres. This reuse is exactly why you create a connection pool once at module scope rather than per request (see Connecting to Postgres).
^[a-z0-9]{1,20}$
Functions支持长期运行但仍为无服务器——它们是请求/响应运行时,而非后台任务运行器。硬限制如下:
- 首字节时间:15分钟。处理程序必须在收到请求后的15分钟内开始返回响应。大多数处理程序在几秒内完成;15分钟的上限是为了给图像/视频生成等Agent工作负载留出空间。
- 心跳:15分钟。打开的WebSocket/SSE连接只要有数据流动就会保持活跃。只有当连接静默时才会触发超时——每15分钟至少发送一个字节以保持静默流活跃。
- :15分钟。通过
waitUntil注册的任务会在响应发送后保持调用活跃,最长15分钟——用于分析写入和审计日志等清理工作,而非后台任务运行器。(预览期间,waitUntil中的@neon/functions目前是存根。)waitUntil - 空闲驱逐。无活跃连接时,Neon会关闭函数;也可能因操作原因(如维护,或将函数移动到不同的计算节点)驱逐/重启——活跃函数可先运行数小时。将驱逐视为进程重启——WebSocket/SSE客户端必须重新连接。Neon在驱逐前发送,因此
SIGINT处理程序可让您检测到函数即将被驱逐并运行任何最后清理工作。您无需专门关闭Postgres连接——Neon的池化器会自行回收这些连接。process.on("SIGINT", ...) - 运行时:Node.js 24,预览期间内存固定为2048 MiB。Slug必须匹配。隔离实例会在多个请求间复用——同一隔离实例上可能同时有多个请求在处理(在Node的单线程事件循环上交错执行),负载下运行时会并行运行多个隔离实例,每个实例都有自己的模块状态副本。因此模块级状态是每个隔离实例独有的(由该隔离实例处理的所有请求共享),且仅在内存中——必须在Postgres中持久化任何需在驱逐后保留的数据。这种复用正是您在模块级创建一次连接池而非每个请求创建一次的原因(请查看连接Postgres)。
^[a-z0-9]{1,20}$
Functions as an Agent Backend (Next.js and Similar Frameworks)
作为Agent后端的Functions(Next.js及类似框架)
A Neon Function is a great home for an AI agent precisely because it doesn't time out the way lambda-style serverless does (15-minute budget, see Timeouts and Runtime Limits). But that advantage disappears the moment you proxy the agent stream through your web app's backend — a Next.js route handler, Remix/SvelteKit/Nuxt action, etc. hosted on Vercel, Netlify, Cloudflare, and the like. Those platforms cap serverless/edge execution at short windows (often ~10–60s, sometimes up to ~300s), so a long agent or image/video generation stream gets cut off mid-response even though the Neon Function would happily keep going.
Building the agent itself. The Vercel AI SDK and Mastra are the recommended ways to build the agent — point either at the Neon AI Gateway (see the skill) for one credential across every model, with no extra provider keys. For a complete AI SDK agent running as a Function (streaming , multi-step tool calling next to Postgres, and persisting generated images to Object Storage), see references/ai-sdk.md; for the Mastra equivalent with built-in tracing, see references/mastra-studio.md.
neon-ai-gatewaytoUIMessageStreamResponseThe fix: call the function directly from the client. Don't route the long request through your app server.
Browser ──(Authorization: Bearer <JWT>)──▶ Neon Function (agent) ✅ no host timeout
Browser ──▶ your app backend ──▶ Neon Function ❌ host cuts the stream- Mint a short-lived JWT on your app backend (e.g. better-auth's plugin, NextAuth, or your own signer) — that call is fast and well within host limits.
jwt - Hand the token to the client and have it call the Neon Function directly (cross-origin), e.g. with the Vercel AI SDK: where
new DefaultChatTransport({ api: NEON_FUNCTION_URL, fetch })attachesfetch. Your app server is never in the path of the long stream.Authorization: Bearer <token> - Add CORS so the browser can reach it (handle , set
OPTIONS/Access-Control-Allow-Origin).-Headers
[!WARNING] A Neon Function has a public HTTPS URL — it is reachable by anyone. A direct client→function call means there is no app backend in front of it to gate access, so you must authenticate the function yourself. Verify a JWT (e.g. against your app's JWKS), check a shared secret / API key, or validate a session token at the top of the handler and reject anything else. Never deploy an unauthenticated agent.
typescript
// src/index.ts — verify the caller before doing any work
import { createRemoteJWKSet, jwtVerify } from "jose";
const jwks = createRemoteJWKSet(new URL(`${process.env.AUTH_BASE_URL}/api/auth/jwks`));
export default {
async fetch(request: Request) {
if (request.method === "OPTIONS") return new Response(null, { status: 204, headers: cors(request) });
const auth = request.headers.get("authorization");
if (!auth?.toLowerCase().startsWith("bearer ")) {
return new Response("Unauthorized", { status: 401, headers: cors(request) });
}
try {
const { payload } = await jwtVerify(auth.slice(7), jwks, {
issuer: process.env.AUTH_BASE_URL,
audience: process.env.AUTH_BASE_URL,
});
const userId = payload.sub; // scope the agent to this user
// ... run the agent, return result.toUIMessageStreamResponse({ headers: cors(request) })
} catch {
return new Response("Unauthorized", { status: 401, headers: cors(request) });
}
},
};Pass the JWKS/issuer URL to the function via its (see Environment Variables). Persist anything you need to keep (generated images, history) in Postgres — module state doesn't survive eviction.
envNeon Function是AI Agent的理想宿主,正是因为它不会像Lambda风格无服务器那样超时(15分钟预算,请查看超时和运行时限制)。但一旦您通过Web应用后端代理Agent流(如Vercel、Netlify、Cloudflare等平台上的Next.js路由处理程序、Remix/SvelteKit/Nuxt操作等),这种优势就会消失。这些平台将无服务器/边缘执行限制在较短的窗口内(通常约10-60秒,有时最多约300秒),因此即使Neon Function可以持续运行,长期Agent或图像/视频生成流也会在响应中途被切断。
构建Agent本身。推荐使用Vercel AI SDK和Mastra构建Agent——将两者指向Neon AI网关(请查看技能),即可通过一个凭据访问所有模型,无需额外的提供商密钥。如需完整的AI SDK Agent作为Function运行(流式、Postgres旁的多步工具调用、将生成的图像持久化到对象存储),请查看references/ai-sdk.md;如需带有内置追踪的Mastra等效方案,请查看references/mastra-studio.md。
neon-ai-gatewaytoUIMessageStreamResponse解决方案:从客户端直接调用函数。不要通过应用服务器路由长请求。
浏览器 ──(Authorization: Bearer <JWT>)──▶ Neon Function (agent) ✅ 无宿主超时
浏览器 ──▶ 您的应用后端 ──▶ Neon Function ❌ 宿主切断流- 在应用后端生成短期JWT(如better-auth的插件、NextAuth或您自己的签名器)——该调用速度快,完全在宿主限制内。
jwt - 将令牌交给客户端,让客户端直接(跨域)调用Neon Function,例如使用Vercel AI SDK:,其中
new DefaultChatTransport({ api: NEON_FUNCTION_URL, fetch })附加fetch。您的应用服务器永远不会参与长流的路径。Authorization: Bearer <token> - 添加CORS以便浏览器可以访问(处理请求,设置
OPTIONS/Access-Control-Allow-Origin)。-Headers
[!WARNING] Neon Function有一个公开HTTPS URL——任何人都可以访问。客户端→函数的直接调用意味着没有应用后端在前面进行访问控制,因此您必须自行对函数进行身份验证。在处理程序顶部验证JWT(如针对您应用的JWKS)、检查共享密钥/API密钥,或验证会话令牌,拒绝其他任何请求。切勿部署未认证的Agent。
typescript
// src/index.ts —— 在执行任何工作前验证调用者
import { createRemoteJWKSet, jwtVerify } from "jose";
const jwks = createRemoteJWKSet(new URL(`${process.env.AUTH_BASE_URL}/api/auth/jwks`));
export default {
async fetch(request: Request) {
if (request.method === "OPTIONS") return new Response(null, { status: 204, headers: cors(request) });
const auth = request.headers.get("authorization");
if (!auth?.toLowerCase().startsWith("bearer ")) {
return new Response("Unauthorized", { status: 401, headers: cors(request) });
}
try {
const { payload } = await jwtVerify(auth.slice(7), jwks, {
issuer: process.env.AUTH_BASE_URL,
audience: process.env.AUTH_BASE_URL,
});
const userId = payload.sub; // 将Agent限定为该用户
// ... 运行Agent,返回result.toUIMessageStreamResponse({ headers: cors(request) })
} catch {
return new Response("Unauthorized", { status: 401, headers: cors(request) });
}
},
};通过函数的传递JWKS/颁发者URL(请查看环境变量)。在Postgres中持久化任何需要保留的内容(生成的图像、历史记录)——模块状态无法在驱逐后存活。
envWebSocket Servers
WebSocket服务器
A WebSocket server is the canonical Functions workload: a long-running handler holds connections open in-process, with no external state store needed to keep a stream coherent. The connection stays alive as long as bytes flow (15-minute heartbeat, see Timeouts).
Upgrade from inside . Call from and return the response it gives you. There is one entrypoint and no WebSocket dependency to install:
fetchupgradeWebSocket(request)@neon/functionstypescript
import { upgradeWebSocket } from "@neon/functions";
export default {
async fetch(req: Request): Promise<Response> {
if (req.headers.get("upgrade")?.toLowerCase() !== "websocket") {
return new Response("expected a websocket upgrade", { status: 426 });
}
const { socket, response } = upgradeWebSocket(req);
socket.addEventListener("message", (event) => socket.send(event.data));
return response;
},
};socketWebSocketaddEventListeneronopenonmessageoncloseonerrorCONNECTING101responseThree rules that matter:
- Return unchanged. A
responsecan't be built as a plain101(the fetch spec caps constructed responses at 200–599), so the runtime hands back an object carrying the pending upgrade.Response, or rebuilding it withclone()as response-rewriting middleware does, discards the upgrade and fails the request.new Response(res.body, res) - Refuse a handshake by returning an ordinary . A
Response,401or403is relayed to the client as-is. That is how you gate a socket.404 - defaults to
binaryType, not the browser's"arraybuffer"."blob"is aevent.datafor text frames and anstringfor binary ones, so branch onArrayBuffer.typeof
With auth. Browsers can't set headers on a WebSocket, so authenticate with a query param (verify it the same way as the agent backend: against your JWKS) and refuse before upgrading:
?token=jwtVerifytypescript
// src/index.ts
import { upgradeWebSocket } from "@neon/functions";
const clients = new Set<WebSocket>();
export default {
async fetch(request: Request): Promise<Response> {
if (request.headers.get("upgrade")?.toLowerCase() !== "websocket") {
return new Response("WebSocket endpoint — connect with ?token=<jwt>");
}
const url = new URL(request.url);
const identity = await verifyToken(url.searchParams.get("token"));
if (!identity) return new Response("unauthorized", { status: 401 });
const { socket, response } = upgradeWebSocket(request);
clients.add(socket);
socket.addEventListener("close", () => clients.delete(socket));
socket.addEventListener("message", (event) => {
if (typeof event.data !== "string") return;
persist(identity.id, event.data); // fan out to every isolate — see below
});
return response;
},
};Subprotocols. Pass to select one the client offered; it is echoed in and exposed as . Selecting one the client did not offer throws a . Omit it and no protocol is negotiated. No extensions are negotiated either — is always and is not available.
{ protocol }Sec-WebSocket-Protocolsocket.protocolTypeErrorsocket.extensions""permessage-deflateHono. Nothing special is needed: takes a , so call it inside a route with and return the response. Auth and everything else is ordinary middleware.
upgradeWebSocketRequestc.req.rawtypescript
// src/index.ts
import { Hono } from "hono";
import { upgradeWebSocket } from "@neon/functions";
const app = new Hono();
app.get("/", (c) => c.text("ok"));
app.get("/ws", async (c) => {
const identity = await verifyToken(c.req.query("token"));
if (!identity) return c.text("Unauthorized", 401);
const { socket, response } = upgradeWebSocket(c.req.raw);
socket.addEventListener("open", () => socket.send("welcome"));
socket.addEventListener("message", (event) => socket.send(`echo: ${event.data}`));
return response;
});
export default { fetch: (request: Request) => app.fetch(request) };WebSocket服务器是典型的Functions工作负载:长期运行的处理程序在进程内保持连接打开,无需外部状态存储即可保持流的连贯性。只要有字节流动,连接就会保持活跃(15分钟心跳,请查看超时)。
typescript
import { upgradeWebSocket } from "@neon/functions";
export default {
async fetch(req: Request): Promise<Response> {
if (req.headers.get("upgrade")?.toLowerCase() !== "websocket") {
return new Response("expected a websocket upgrade", { status: 426 });
}
const { socket, response } = upgradeWebSocket(req);
socket.addEventListener("message", (event) => socket.send(event.data));
return response;
},
};socketWebSocketaddEventListeneronopenonmessageoncloseonerrorCONNECTINGresponse101需要注意三个规则:
- 原样返回。无法通过普通
response构建Response状态码(fetch规范将构造的响应限制在200-599之间),因此运行时会返回一个携带待升级状态的对象。101或像响应重写中间件那样用clone()重建,会丢弃升级状态并导致请求失败。new Response(res.body, res) - 通过返回普通拒绝握手。
Response、401或403会原样转发给客户端。这是您限制socket访问的方式。404 - 默认为
binaryType,而非浏览器的"arraybuffer"。"blob"对于文本帧是event.data,对于二进制帧是string,因此需根据ArrayBuffer分支处理。typeof
typescript
// src/index.ts
import { upgradeWebSocket } from "@neon/functions";
const clients = new Set<WebSocket>();
export default {
async fetch(request: Request): Promise<Response> {
if (request.headers.get("upgrade")?.toLowerCase() !== "websocket") {
return new Response("WebSocket endpoint — connect with ?token=<jwt>");
}
const url = new URL(request.url);
const identity = await verifyToken(url.searchParams.get("token"));
if (!identity) return new Response("unauthorized", { status: 401 });
const { socket, response } = upgradeWebSocket(request);
clients.add(socket);
socket.addEventListener("close", () => clients.delete(socket));
socket.addEventListener("message", (event) => {
if (typeof event.data !== "string") return;
persist(identity.id, event.data); // 扩散到每个隔离实例——请查看下文
});
return response;
},
};子协议。传递以选择客户端提供的子协议;它会在中回显,并作为暴露。选择客户端未提供的子协议会抛出。省略则不协商协议。也不协商扩展——始终为,不支持。
{ protocol }Sec-WebSocket-Protocolsocket.protocolTypeErrorsocket.extensions""permessage-deflateHono集成。无需特殊操作:接受,因此在路由中调用并返回响应即可。身份验证和其他所有操作都是普通中间件。
upgradeWebSocketRequestc.req.rawtypescript
// src/index.ts
import { Hono } from "hono";
import { upgradeWebSocket } from "@neon/functions";
const app = new Hono();
app.get("/", (c) => c.text("ok"));
app.get("/ws", async (c) => {
const identity = await verifyToken(c.req.query("token"));
if (!identity) return c.text("Unauthorized", 401);
const { socket, response } = upgradeWebSocket(c.req.raw);
socket.addEventListener("open", () => socket.send("welcome"));
socket.addEventListener("message", (event) => socket.send(`echo: ${event.data}`));
return response;
});
export default { fetch: (request: Request) => app.fetch(request) };Heartbeat (keep the socket alive)
心跳(保持socket活跃)
A connection stays open only while bytes flow: Neon evicts a silent stream after 15 minutes (Timeouts and Runtime Limits), and intermediary proxies / load balancers are usually far stricter (often tens of seconds). Don't rely on the app being chatty enough — send a periodic keepalive from the server so the socket never goes quiet.
The standard interface has no , so send an application-level message the client ignores:
WebSocketping()typescript
const HEARTBEAT_MS = 25_000; // comfortably under proxy idle timeouts
const beat = setInterval(() => {
for (const socket of clients) {
if (socket.readyState === socket.OPEN) socket.send('{"type":"ping"}');
}
}, HEARTBEAT_MS);
beat.unref?.();The client should skip these when handling messages. The server does answer a client-sent ping frame with a pong automatically, so a browser client can drive the heartbeat instead if you'd rather not filter messages.
连接仅在字节流动时保持打开:Neon会在15分钟后驱逐静默流(超时和运行时限制),而中间代理/负载均衡器通常严格得多(通常为几十秒)。不要依赖应用足够活跃——从服务器定期发送保活消息,使socket永不静默。
标准接口没有方法,因此发送客户端忽略的应用级消息:
WebSocketping()typescript
const HEARTBEAT_MS = 25_000; // 远低于代理空闲超时
const beat = setInterval(() => {
for (const socket of clients) {
if (socket.readyState === socket.OPEN) socket.send('{"type":"ping"}');
}
}, HEARTBEAT_MS);
beat.unref?.();客户端处理消息时应跳过这些消息。服务器会自动用pong帧响应客户端发送的ping帧,因此如果您不想过滤消息,也可以让浏览器客户端驱动心跳。
Keeping clients in sync across isolates (do not skip this)
在隔离实例间保持客户端同步(请勿跳过)
Under load the runtime runs several isolates in parallel, each with its own copy of module state — so each isolate has its own set. Broadcasting only to that local set means a client on isolate A never sees an event produced on isolate B, and the feed silently fractures. It's easy to miss: runs a single process (one isolate), so in-process broadcast always looks fine locally but breaks in production, where concurrent connections spread across many isolates.
clientsneon devModule state doesn't survive eviction anyway, so Postgres is the shared source of truth. Pick a fan-out strategy. In every snippet below, is a pooled client and is this isolate's of live connections.
poolpgclientsSet1. Poll Postgres — the default, and the only option that keeps Scale to Zero. Each isolate re-reads the shared state (or rows past a cursor) on a short interval and pushes changes to its own clients. One query per isolate per tick (not per client), and none when the isolate has no clients — so an idle compute still suspends.
typescript
let lastId = 0;
const poller = setInterval(async () => {
if (clients.size === 0) return; // no clients here → no query → compute can scale to zero
const { rows } = await pool.query(
"SELECT id, payload FROM events WHERE id > $1 ORDER BY id",
[lastId],
);
for (const { id, payload } of rows) {
lastId = id;
for (const socket of clients) {
if (socket.readyState === socket.OPEN) socket.send(payload);
}
}
}, 1000);
poller.unref?.();- Latency: up to the interval (~1s) — fine for counters, chat, and dashboards.
- Scaling: database load grows with the number of live isolates, not clients. Keep the cursor on an indexed /
serialPK and the interval sane.bigserial - Scale to Zero: ✅ preserved — polling stops when no clients are connected, so the compute suspends on its normal timer.
2. / — lowest latency, but requires disabling Scale to Zero. Each isolate s on a channel over a dedicated unpooled connection; broadcasting is , so every isolate (including the sender's) re-pushes to its sockets. Near-instant — but the listener holds an idle connection that does not count as active, so Scale to Zero suspends the compute and drops it, silently killing the feed. Only use it on an always-on compute (Scale to Zero disabled — a paid-plan setting).
LISTENNOTIFYLISTENNOTIFYtypescript
import { Pool, Client } from "pg";
const pool = new Pool({ connectionString: process.env.DATABASE_URL, max: 5 });
const CHANNEL = "chat_events";
// One dedicated DIRECT connection per isolate, just to receive events.
// Use DATABASE_URL_UNPOOLED — LISTEN needs a real session, not a pooled one.
const listener = new Client({ connectionString: process.env.DATABASE_URL_UNPOOLED });
listener.connect().then(() => listener.query(`LISTEN ${CHANNEL}`));
listener.on("notification", (msg) => {
if (!msg.payload) return;
for (const socket of clients) {
if (socket.readyState === socket.OPEN) socket.send(msg.payload);
}
});
// Broadcast by NOTIFYing through the pool — every isolate's listener fires.
function broadcast(event: unknown) {
return pool.query("SELECT pg_notify($1, $2)", [CHANNEL, JSON.stringify(event)]);
}3. External pub/sub (e.g. Upstash Redis) — best at scale. For high fan-out, sub-second latency at large connection counts, or multi-region, publish/subscribe through a dedicated broker. Highest throughput, and it doesn't touch Postgres or block Scale to Zero — at the cost of another service to run.
Rule of thumb: start with polling (works with Scale to Zero, no extra infra); switch to / only on always-on compute that needs sub-second latency; move to Redis when fan-out outgrows Postgres.
LISTENNOTIFY负载下运行时会并行运行多个隔离实例,每个实例都有自己的模块状态副本——因此每个隔离实例都有自己的集合。仅向本地集合广播意味着隔离实例A上的客户端永远看不到隔离实例B上产生的事件,流会静默断裂。这很容易被忽略:运行单个进程(一个隔离实例),因此进程内广播在本地看起来总是正常的,但在生产环境中,并发连接会分散到多个隔离实例,从而导致问题。
clientsneon dev模块状态无论如何都无法在驱逐后存活,因此Postgres是共享的事实来源。选择一种扩散策略。以下每个代码片段中,是池化的客户端,是当前隔离实例的活跃连接。
poolpgclientsSet1. 轮询Postgres —— 默认方案,也是唯一支持缩容至零的方案。每个隔离实例在短时间间隔内重新读取共享状态(或游标后的行),并将更改推送给自己的客户端。每个隔离实例每个周期一次查询(而非每个客户端一次),且当隔离实例没有客户端时不查询——因此空闲计算仍可暂停。
typescript
let lastId = 0;
const poller = setInterval(async () => {
if (clients.size === 0) return; // 此处无客户端 → 无查询 → 计算可缩容至零
const { rows } = await pool.query(
"SELECT id, payload FROM events WHERE id > $1 ORDER BY id",
[lastId],
);
for (const { id, payload } of rows) {
lastId = id;
for (const socket of clients) {
if (socket.readyState === socket.OPEN) socket.send(payload);
}
}
}, 1000);
poller.unref?.();- 延迟:最多为间隔时间(约1秒)——适用于计数器、聊天和仪表板。
- 扩展性:数据库负载随活跃隔离实例的数量增长,而非客户端数量。将游标放在带索引的/
serial主键上,并设置合理的间隔时间。bigserial - 缩容至零:✅ 保留——无客户端连接时轮询停止,因此计算会按正常计时器暂停。
2. / —— 延迟最低,但需禁用缩容至零。每个隔离实例通过专用的非池化连接一个通道;广播通过实现,因此每个隔离实例(包括发送者的隔离实例)都会将事件重新推送给自己的socket。几乎即时——但监听器保持的空闲连接不被视为活跃,因此缩容至零会暂停计算并断开连接,静默中断流。仅在始终运行的计算(禁用缩容至零——付费计划设置)上使用。
LISTENNOTIFYLISTENNOTIFYtypescript
import { Pool, Client } from "pg";
const pool = new Pool({ connectionString: process.env.DATABASE_URL, max: 5 });
const CHANNEL = "chat_events";
// 每个隔离实例一个专用的直接连接,仅用于接收事件。
// 使用DATABASE_URL_UNPOOLED —— LISTEN需要真实会话,而非池化连接。
const listener = new Client({ connectionString: process.env.DATABASE_URL_UNPOOLED });
listener.connect().then(() => listener.query(`LISTEN ${CHANNEL}`));
listener.on("notification", (msg) => {
if (!msg.payload) return;
for (const socket of clients) {
if (socket.readyState === socket.OPEN) socket.send(msg.payload);
}
});
// 通过池化连接NOTIFY进行广播——每个隔离实例的监听器都会触发。
function broadcast(event: unknown) {
return pool.query("SELECT pg_notify($1, $2)", [CHANNEL, JSON.stringify(event)]);
}3. 外部发布/订阅(如Upstash Redis)—— 大规模场景下最佳。对于高扩散、大连接数下的亚秒级延迟,或多区域场景,通过专用代理进行发布/订阅。吞吐量最高,且不涉及Postgres或阻止缩容至零——代价是需要运行另一个服务。
经验法则:从轮询开始(支持缩容至零,无需额外基础设施);仅在需要亚秒级延迟的始终运行计算上切换到/;当扩散规模超出Postgres能力时迁移到Redis。
LISTENNOTIFYClient must reconnect
客户端必须重新连接
Idle functions are evicted (and isolates restart for operational reasons), so a client's socket will drop — treat reconnection as normal, not exceptional. Reconnect with exponential backoff, capped, and re-mint a fresh token on every attempt (tokens are short-lived, so a stale one fails the auth check):
upgradetypescript
let closed = false, retry = 0, timer: ReturnType<typeof setTimeout>;
async function connect() {
if (closed) return;
const token = await getToken(); // re-mint each attempt; short-lived
const ws = new WebSocket(`${WS_URL}?token=${encodeURIComponent(token)}`);
ws.onopen = () => { retry = 0; }; // reset backoff on success
ws.onmessage = (e) => { /* apply the event */ };
ws.onclose = () => {
if (!closed) timer = setTimeout(connect, Math.min(1000 * 2 ** retry++, 15000));
};
ws.onerror = () => ws.close(); // let onclose drive the retry
}
connect();Together — inside , JWT auth over , cross-isolate fan-out, and client backoff — these compose into a complete realtime chat backend on a single function.
upgradeWebSocketfetch?token=空闲函数会被驱逐(且隔离实例会因操作原因重启),因此客户端的socket必然会断开——将重新连接视为正常情况,而非异常情况。使用指数退避进行重新连接,设置上限,并且每次尝试都重新生成新令牌(令牌是短期的,因此过期令牌会在升级身份验证检查中失败):
typescript
let closed = false, retry = 0, timer: ReturnType<typeof setTimeout>;
async function connect() {
if (closed) return;
const token = await getToken(); // 每次尝试重新生成;短期有效
const ws = new WebSocket(`${WS_URL}?token=${encodeURIComponent(token)}`);
ws.onopen = () => { retry = 0; }; // 成功时重置退避
ws.onmessage = (e) => { /* 应用事件 */ };
ws.onclose = () => {
if (!closed) timer = setTimeout(connect, Math.min(1000 * 2 ** retry++, 15000));
};
ws.onerror = () => ws.close(); // 让onclose驱动重试
}
connect();结合内部的、通过的JWT身份验证、跨隔离实例扩散和客户端退避,这些组件可组成一个完整的实时聊天后端,仅需一个函数。
fetchupgradeWebSocket?token=Server-Sent Events (SSE)
服务器发送事件(SSE)
When you only need server → client streaming (live counters, notifications, progress, token streams), SSE is simpler than a WebSocket and needs no upgrade at all: a plain handler returns a whose body is a with , and the runtime holds it open as long as bytes flow. The browser consumes it with , which reconnects on its own — so there's no client backoff to write.
fetchResponseReadableStreamContent-Type: text/event-streamEventSourcetypescript
// src/index.ts — minimal SSE endpoint
const encoder = new TextEncoder();
export default {
fetch: () =>
new Response(
new ReadableStream<Uint8Array>({
start(controller) {
controller.enqueue(encoder.encode("data: hello\n\n"));
const t = setInterval(() => controller.enqueue(encoder.encode(": ping\n\n")), 25_000);
return () => clearInterval(t); // fires when the client disconnects
},
}),
{ headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache, no-transform" } },
),
};The same rules as WebSockets apply. Heartbeat: a stream stays open only while bytes flow — Neon's window is 15 minutes (Timeouts and Runtime Limits) but proxies are usually far stricter, so emit a comment every ~25–30s (shown above) to keep idle streams from being dropped. Keep state in Postgres, and fan out across isolates using one of the sync strategies (hold a of stream controllers and to each). is GET-only and can't set headers, so authenticate with a query param or cookie, exactly like the WebSocket case. references/sse.md has the full pattern — Hono variant, cross-isolate fan-out, wire format, client, and caveats.
: ping\n\nSetenqueueEventSource?token=当您仅需要服务器→客户端流式传输(实时计数器、通知、进度、令牌流)时,SSE比WebSocket更简单,且无需任何升级:普通处理程序返回一个,其主体是带有的,运行时会在字节流动期间保持连接打开。浏览器使用消费它,它会自动重新连接——因此无需编写客户端退避逻辑。
fetchResponseContent-Type: text/event-streamReadableStreamEventSourcetypescript
// src/index.ts —— 极简SSE端点
const encoder = new TextEncoder();
export default {
fetch: () =>
new Response(
new ReadableStream<Uint8Array>({
start(controller) {
controller.enqueue(encoder.encode("data: hello\
\
"));
const t = setInterval(() => controller.enqueue(encoder.encode(": ping\
\
")), 25_000);
return () => clearInterval(t); // 客户端断开连接时触发
},
}),
{ headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache, no-transform" } },
),
};与WebSocket适用相同规则。心跳:流仅在字节流动时保持打开——Neon的窗口是15分钟(超时和运行时限制),但代理通常严格得多,因此每隔约25-30秒发送一条注释(如上所示),以防止空闲流被断开。将状态保存在Postgres中,并使用同步策略之一在隔离实例间扩散(持有的流控制器并向每个控制器)。仅支持GET,无法设置标头,因此使用查询参数或cookie进行身份验证,与WebSocket场景完全相同。references/sse.md包含完整模式——Hono变体、跨隔离实例扩散、有线格式、客户端和注意事项。
: ping\ \ SetenqueueEventSource?token=MCP Servers
MCP服务器
An MCP server is a natural Functions workload: a long-running HTTP handler that exposes tools to AI clients (Cursor, Claude, ChatGPT, agents), with those tools reading and writing the branch's Postgres right next to the compute. MCP's streamable HTTP transport is a plain / on a single endpoint (conventionally ), so it maps onto a function's handler with no method or extra protocol.
POSTGET/mcpfetchupgradeThe simplest host is a Hono app using the official plus , which bridges the transport to a route. Build the server, register its tools, and create the transport once at module scope, then hand every request to it:
@modelcontextprotocol/sdk@hono/mcp/mcptypescript
const transport = new StreamableHTTPTransport();
app.all("/mcp", async (c) => {
if (!mcpServer.isConnected()) await mcpServer.connect(transport);
return transport.handleRequest(c);
});Because the function's URL is public, authenticate before connecting the transport — Better Auth covers both OAuth (its MCP plugin makes your app the authorization server so third-party clients self-authorize per the MCP spec) and a simpler API-key / session-JWT check for your own callers. references/mcp.md has the full pattern — server with Postgres-backed tools via Drizzle, both Better Auth auth options, and testing with / .
mcporteradd-mcpMCP服务器是天然的Functions工作负载:长期运行的HTTP处理程序向AI客户端(Cursor、Claude、ChatGPT、Agent)公开工具,这些工具在计算资源旁读取和写入分支的Postgres。MCP的可流式HTTP传输是单个端点上的普通/(通常为),因此无需升级方法或额外协议即可映射到函数的处理程序。
POSTGET/mcpfetch最简单的宿主是使用官方和的Hono应用,后者将传输桥接到路由。构建服务器、注册工具并在模块级创建一次传输,然后将所有请求交给它:
@modelcontextprotocol/sdk@hono/mcp/mcptypescript
const transport = new StreamableHTTPTransport();
app.all("/mcp", async (c) => {
if (!mcpServer.isConnected()) await mcpServer.connect(transport);
return transport.handleRequest(c);
});由于函数的URL是公开的,连接传输前必须进行身份验证——Better Auth涵盖OAuth(其MCP插件使您的应用成为授权服务器,因此第三方客户端可根据MCP规范自行授权)和针对您自己调用者的更简单API密钥/会话JWT检查。references/mcp.md包含完整模式——通过Drizzle实现Postgres支持的工具服务器、两种Better Auth身份验证选项,以及使用/进行测试。
mcporteradd-mcpIntegrations and Observability
集成与可观测性
Built-in branch logs
内置分支日志
bash
neon logs query --branch production --source function --since 1hFunctions is one of the two sources branch logs cover today, alongside Object Storage. Logs are scoped to a single branch, so pass when the deployed function isn't on the branch you're checked out on. Everything else about logs — the required CLI version, filters, the SDK, and the Loki-compatible read API — is in the parent skill's Observability section.
--branchneonbash
neon logs query --branch production --source function --since 1hFunctions是当前分支日志涵盖的两个来源之一,另一个是对象存储。日志范围限定为单个分支,因此当部署的函数不在您当前检出的分支上时,请传递。关于日志的其他所有内容——所需的CLI版本、过滤器、SDK和兼容Loki的读取API——都在父级技能的可观测性部分。
--branchneonApplication instrumentation
应用 instrumentation
A function is a long-lived Node.js process running a web-standard request/response handler, so standard Node integration SDKs work unchanged. Initialize them once at module load, gated on an env var so local dev and unconfigured branches stay a no-op, and pass secrets via or .
--envneon.tsenv- Sentry — error monitoring across the HTTP framework, the function runtime, and an agent's own caught/fallback failures (the long-running case Functions target): see references/sentry.md.
- Mastra Studio (Mastra Cloud) — run a Mastra agent on a function and ship its traces to a Studio project for observability: see references/mastra-studio.md.
函数是运行符合Web标准的请求/响应处理程序的长期Node.js进程,因此标准Node集成SDK可直接使用。在模块加载时初始化一次,通过环境变量控制,使本地开发和未配置的分支保持无操作状态,并通过或的传递密钥。
--envneon.tsenv- Sentry —— 跨HTTP框架、函数运行时和Agent自身捕获/回退失败的错误监控(Functions针对的长期运行场景):请查看references/sentry.md。
- Mastra Studio(Mastra Cloud) —— 在函数上运行Mastra Agent并将其追踪数据发送到Studio项目以实现可观测性:请查看references/mastra-studio.md。
Neon Documentation
Neon文档
The Neon documentation is the source of truth and Functions is evolving rapidly, so always verify against the official docs. Any doc page can be fetched as markdown by appending to the URL or by requesting . Find the right page from the docs index (https://neon.com/docs/llms.txt) and the changelog announcements.
.mdAccept: text/markdownNeon文档是事实来源,且Functions正在快速演进,因此请始终对照官方文档进行验证。任何文档页面都可通过在URL后追加或请求获取为markdown格式。从文档索引(https://neon.com/docs/llms.txt)和变更日志公告中找到正确的页面。
.mdAccept: text/markdownFurther Reading
进一步阅读
- https://neon.com/docs/compute/functions/overview.md
- https://neon.com/docs/compute/functions/get-started.md
- https://neon.com/docs/compute/functions/deploy.md
- https://neon.com/docs/compute/functions/environment-variables.md
- https://neon.com/docs/compute/functions/reference/neon-ts.md
- https://neon.com/docs/compute/functions/reference/runtime-limits.md
- https://neon.com/docs/compute/functions/preview-access.md
- https://neon.com/docs/compute/functions/overview.md
- https://neon.com/docs/compute/functions/get-started.md
- https://neon.com/docs/compute/functions/deploy.md
- https://neon.com/docs/compute/functions/environment-variables.md
- https://neon.com/docs/compute/functions/reference/neon-ts.md
- https://neon.com/docs/compute/functions/reference/runtime-limits.md
- https://neon.com/docs/compute/functions/preview-access.md ",