neon-functions

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese
FIRST: Use the parent
neon
skill for a Neon overview, getting started with Neon, Neon development best practices, and more.
If the
neon
skill is not installed, fetch it from https://neon.com/docs/ai/skills/neon/SKILL.md or install it with:
bash
npx skills add neondatabase/agent-skills --skill neon
首先:使用父级
neon
技能获取Neon概览、快速入门、Neon开发最佳实践等内容。
bash
npx skills add neondatabase/agent-skills --skill neon

Neon Functions

Neon Functions

This is a public beta feature and only available in
us-east-2
.
Neon 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
DATABASE_URL
injected automatically. You deploy and manage them through the same Neon CLI,
neon.ts
, and API you already use.
Use 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
neon dev
loop, or a precise answer from the official Neon docs.
这是一项公开测试版功能,仅在
us-east-2
区域可用。
Neon Functions是部署在Neon分支上的长期运行Node.js HTTP处理程序。每个函数都有一个公开的HTTPS URL,与您的数据库在同一区域运行——如果分支包含Postgres,会自动注入
DATABASE_URL
。您可以通过已熟悉的Neon CLI、
neon.ts
和API来部署和管理这些函数。
使用本技能帮助用户定义、本地运行、部署和管理数据库旁的函数。提供已部署函数的调用URL、可用的本地
neon dev
循环,或来自Neon官方文档的精准答案。

When 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
    pg
    pool, an in-memory counter) persists across requests on the same isolate.
  • 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.
    DATABASE_URL
    is injected for you.
  • 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
    waitUntil
    (analytics, audit logs) all fit.
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
us-east-2
today, this isn't the right tool yet (see Timeouts and Runtime Limits and Availability).
当工作负载为请求/响应处理程序,且受益于持续运行和贴近数据时,可选择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
    执行的即发即弃后续任务(如分析、审计日志)都适用。
如果工作负载是纯静态站点、需要独立生命周期和取消机制的定时/后台任务,或目前必须在
us-east-2
以外区域运行,那么本工具暂时不适用(请查看超时和运行时限制可用性)。

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
    fetch(request)
    method returning a
    Response
    (Workers/WinterTC-compatible). A Hono app exports exactly that shape, so
    export default app
    just works. Runs on Node.js 24, so all Node APIs are available.
  • Close to your database — Runs in the branch's region;
    DATABASE_URL
    injected automatically when the branch has Postgres.
  • 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
    ,
    neon.ts
    , or the Neon API.
  • 长期运行且无服务器 —— 专为WebSocket服务器(请查看WebSocket服务器)、SSE端点(请查看服务器发送事件(SSE))、长期Agent HTTP流和API构建。空闲时仍可缩容至零。
  • 符合Web标准的处理程序 —— 函数是任何默认导出的、带有
    fetch(request)
    方法并返回
    Response
    的对象(兼容Workers/WinterTC)。Hono应用正好符合此结构,因此
    export default app
    可直接运行。基于Node.js 24构建,支持所有Node API。
  • 贴近数据库 —— 在分支所在区域运行;当分支包含Postgres时自动注入
    DATABASE_URL
  • 可分支同步 —— 每个分支在自己的URL上运行独立的函数版本,对应自己的隔离状态。
  • 统一CLI/API —— 通过
    neon
    neon.ts
    或Neon API进行部署和管理。

Availability

可用性

Check this precondition before setting anything up: Neon Functions is a public beta feature available only on new projects in the
us-east-2
region. Confirm the user's Neon project is a new project in
us-east-2
; it can't be enabled on existing projects. Functions usage isn't billed during the public beta.
在设置前请检查以下前提条件:Neon Functions是公开测试版功能,仅在
us-east-2
区域的新项目中可用。确认用户的Neon项目是
us-east-2
区域的新项目;现有项目无法启用该功能。公开测试期间,Functions使用不收取费用。

Architecture: 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
neon.ts
(see the
neon
skill for the branch-first workflow and
neon.ts
basics). Add
@neon/config
and declare functions under
preview.functions
, keyed by slug:
typescript
// 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
name
for a human-readable label.
A minimal function — a Hono app that queries the branch's Postgres via the injected
DATABASE_URL
:
typescript
// 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
pg
pool at module scope (reused across requests on the same isolate) and keep
max
small (e.g. 5), since each isolate keeps its own pool.
parseEnv(config)
requires every variable the config implies. A function that only talks to Postgres over the pooled URL can scope it to just that key —
parseEnv
then validates and returns only what you asked for (the keys autocomplete from your
neon.ts
):
typescript
const { postgres } = parseEnv(config, ["DATABASE_URL"]); // not the unpooled URL, auth, etc.
const pool = new Pool({ connectionString: postgres.databaseUrl, max: 5 });
Functions在
neon.ts
中声明(请查看
neon
技能了解分支优先工作流和
neon.ts
基础知识)。添加
@neon/config
并在
preview.functions
下声明函数,以slug作为键:
typescript
// 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
设置人类可读的标签。
一个极简函数——通过注入的
DATABASE_URL
查询分支Postgres的Hono应用:
typescript
// 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;
在模块级创建
pg
连接池(同一隔离实例的多个请求间复用),并将
max
设置为较小值(如5),因为每个隔离实例都有自己的连接池。
parseEnv(config)
需要配置中隐含的所有变量。仅通过池化URL与Postgres交互的函数可将其范围限定为该键——
parseEnv
会验证并仅返回您请求的内容(键会从
neon.ts
自动补全):
typescript
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 branch
To deploy a single function without
neon.ts
:
neon functions deploy <slug> --src src/index.ts
(
--src
takes either the entry file or a directory containing
index.ts
,
index.mjs
, or
index.js
). Retrieve the public URL with
neon functions get <slug>
(the
invocation_url
field, of the form
https://<branch_id>-<slug>.compute.c-1.us-east-2.aws.neon.tech
). Manage with
neon functions list|get|delete
.
When
neon checkout
creates a new branch and a
neon.ts
is present, it applies the policy automatically — deploying the function to the fresh branch. Checking out an existing branch does not re-deploy; run
neon deploy
explicitly.
bash
neon dev      # 热重载运行neon.ts中的所有函数;注入DATABASE_URL等环境变量
neon deploy   # 使用esbuild打包、上传,并将neon.ts应用到关联分支
无需
neon.ts
即可部署单个函数:
neon functions deploy <slug> --src src/index.ts
--src
接受入口文件或包含
index.ts
index.mjs
index.js
的目录)。使用
neon functions get <slug>
获取公开URL(
invocation_url
字段,格式为
https://<branch_id>-<slug>.compute.c-1.us-east-2.aws.neon.tech
)。使用
neon functions list|get|delete
进行管理。
neon checkout
创建新分支且存在
neon.ts
时,会自动应用策略——将函数部署到新分支。检出现有分支不会重新部署;需显式运行
neon deploy

Neon Infrastructure as Code (
neon.ts
)

Neon基础设施即代码(
neon.ts

The
preview.functions
block from Setup is part of
neon.ts
, Neon's infrastructure-as-code file — one TypeScript file declares every function (its
source
, display
name
, and
env
) alongside any other branch services, in version control (see the
neon
skill for the full reference). Treat it like Terraform for your branch:
bash
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
neon.ts
is present,
neon checkout
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
neon deploy
to apply changes.
Per-branch deploy tuning (e.g.
runtime
) lives in the
branch
closure, keyed by slug, so it can vary by branch without changing which functions exist:
typescript
export default defineConfig({
  preview: {
    functions: { todos: { name: "todo api", source: "src/index.ts" } },
  },
  branch: (branch) => ({
    preview: { functions: { todos: { runtime: "nodejs24" } } },
  }),
});
设置中的
preview.functions
块是
neon.ts
的一部分,
neon.ts
是Neon的基础设施即代码文件——一个TypeScript文件声明所有函数(其
source
、显示
name
env
)以及任何其他分支服务,并纳入版本控制(请查看
neon
技能获取完整参考)。将其视为分支的Terraform:
bash
neon config status   # 打印分支的实时配置(已部署的函数)
neon config plan     # 试运行apply操作的差异
neon config apply    # 打包并部署声明的函数(neon deploy是别名)
Functions是分支范围的:每个分支在自己的URL上运行独立的部署版本。当存在
neon.ts
时,
neon checkout
创建分支时应用策略,因此新的预览/CI分支创建后会自动部署函数。检出现有分支不会重新部署——运行
neon deploy
以应用更改。
每个分支的部署调优(如
runtime
)位于
branch
闭包中,以slug为键,因此无需更改函数即可在不同分支间调整:
typescript
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:
VariableNotes
NEON_BRANCH
The branch name (e.g.
main
,
preview/foo
). Injected on every branch, including the default.
DATABASE_URL
Pooled connection string. Use for most queries. Present only if the branch has Postgres.
DATABASE_URL_UNPOOLED
Direct connection. Use for migrations,
LISTEN
/
NOTIFY
, multi-round-trip transactions.
NEON_AUTH_BASE_URL
Present when Neon Auth is enabled on the branch.
NEON_DATA_API_URL
Present when the Data API is enabled on the branch.
Object storage (
AWS_*
) and AI Gateway (
NEON_AI_GATEWAY_*
) vars are also injected when those services are declared — see the
neon-object-storage
and
neon-ai-gateway
skills.
neon env pull
/
neon-env run
/
neon dev
emit
NEON_BRANCH
(and the connection strings) into your local dev environment too, so local runs mirror the deployed runtime.
Your own secrets are per-deployment. Set them with
--env KEY=VALUE
on
neon functions deploy
(repeatable;
--env KEY=
deletes a key, unmentioned keys carry over), or declare them in
neon.ts
under the function's
env
(resolved at deploy time, so read from
process.env
to avoid hardcoding):
typescript
functions: {
  todos: {
    name: "todo api",
    source: "src/index.ts",
    env: { RESEND_API_KEY: process.env.RESEND_API_KEY! },
  },
}
Load a
.env
before deploy with
neon deploy --env .env.production
. Pull the branch's Neon-managed vars onto disk for local dev with
neon env pull
(
link
/
checkout
do this automatically; pass
--no-env-pull
to skip and use
neon-env run -- <cmd>
for runtime injection). Limits: ≤1,000 vars, ≤64 KiB total, and the
NEON_
prefix is reserved.
Neon在运行时注入分支范围的连接字符串和服务URL——您无需在部署时声明或传递这些变量:
变量名说明
NEON_BRANCH
分支名称(如
main
preview/foo
)。在所有分支(包括默认分支)上都会注入。
DATABASE_URL
池化连接字符串。适用于大多数查询。仅当分支包含Postgres时存在。
DATABASE_URL_UNPOOLED
直接连接字符串。适用于迁移、
LISTEN
/
NOTIFY
、多轮事务。
NEON_AUTH_BASE_URL
当分支启用Neon Auth时存在。
NEON_DATA_API_URL
当分支启用Data API时存在。
当声明对象存储(
AWS_*
)和AI网关(
NEON_AI_GATEWAY_*
)服务时,也会注入相应变量——请查看
neon-object-storage
neon-ai-gateway
技能。
neon env pull
/
neon-env run
/
neon dev
也会将
NEON_BRANCH
(以及连接字符串)注入本地开发环境,使本地运行与部署后的运行时保持一致。
您自己的密钥是每个部署独立的。可在
neon functions deploy
时使用
--env KEY=VALUE
设置(可重复使用;
--env KEY=
删除密钥,未提及的密钥会保留),或在
neon.ts
中函数的
env
下声明(部署时解析,因此从
process.env
读取以避免硬编码):
typescript
functions: {
  todos: {
    name: "todo api",
    source: "src/index.ts",
    env: { RESEND_API_KEY: process.env.RESEND_API_KEY! },
  },
}
部署前使用
neon deploy --env .env.production
加载
.env
文件。使用
neon env pull
将分支的Neon托管变量拉取到本地用于开发(
link
/
checkout
会自动执行此操作;传递
--no-env-pull
可跳过,使用
neon-env run -- <cmd>
进行运行时注入)。限制:最多1000个变量,总大小不超过64 KiB,
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:
  • DATABASE_URL
    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_URL
    by default.
  • DATABASE_URL_UNPOOLED
    direct connection string to the same database. Use it for migrations,
    LISTEN
    /
    NOTIFY
    , and long multi-statement transactions.
Use Drizzle (or another ORM) on top of node-postgres (
pg
)
for queries and schema management — not Neon's serverless driver. Functions are long-running and reuse an isolate across many requests, so a persistent
pg
pool is the right fit; the serverless driver's HTTP transport is meant for fully isolated, lambda-style runtimes.
Create 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
max
small (e.g.
5
): 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
SIGINT
/
SIGTERM
, and Neon's pooler reclaims those connections for you, so an explicit drain handler is redundant.
Reading
process.env.DATABASE_URL
directly works everywhere. The function in Setup instead uses
@neon/env
's
parseEnv(config)
to read the same value in a typed, validated way — either is fine.
当分支包含Postgres时,Neon会在运行时自动注入连接字符串——您无需声明、在部署时传递或硬编码任何内容。常用的两个字符串:
  • DATABASE_URL
    —— 池化连接字符串(通过Neon的连接池路由)。适用于常规请求/响应查询流量。保持无前缀是因为所有Postgres ORM(Drizzle、Prisma、Knex等)默认读取
    DATABASE_URL
  • DATABASE_URL_UNPOOLED
    —— 直接连接到同一数据库的字符串。适用于迁移、
    LISTEN
    /
    NOTIFY
    和长多语句事务。
**在node-postgres(
pg
)之上使用Drizzle(或其他ORM)**进行查询和架构管理——不要使用Neon的无服务器驱动。Functions是长期运行的,会在多个请求间复用隔离实例,因此持久化的
pg
连接池是合适的选择;无服务器驱动的HTTP传输专为完全隔离的Lambda风格运行时设计。
在模块级创建一次连接池并在多个请求间复用——不要为每个请求打开连接:
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连接。
max
设置为较小值(如
5
):每个隔离实例都有自己的连接池,因此Postgres的总连接数随活跃隔离实例的数量扩展。您无需在关闭时关闭连接池——当运行时驱逐隔离实例时会发送
SIGINT
/
SIGTERM
,Neon的池化器会为您回收这些连接,因此显式的 drain 处理程序是多余的。
直接读取
process.env.DATABASE_URL
在任何地方都有效。设置中的函数使用
@neon/env
parseEnv(config)
以类型化、验证的方式读取相同的值——两种方式都可行。

Timeouts 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.
  • waitUntil
    : 15 minutes.
    Work registered with
    waitUntil
    keeps 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. (
    waitUntil
    from
    @neon/functions
    is currently a stub during the preview.)
  • 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
    SIGINT
    before evicting, so a
    process.on("SIGINT", ...)
    handler 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.
  • Runtime: Node.js 24, memory fixed at 2048 MiB during the preview. Slugs must match
    ^[a-z0-9]{1,20}$
    . 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).
Functions支持长期运行但仍为无服务器——它们是请求/响应运行时,而非后台任务运行器。硬限制如下:
  • 首字节时间:15分钟。处理程序必须在收到请求后的15分钟内开始返回响应。大多数处理程序在几秒内完成;15分钟的上限是为了给图像/视频生成等Agent工作负载留出空间。
  • 心跳:15分钟。打开的WebSocket/SSE连接只要有数据流动就会保持活跃。只有当连接静默时才会触发超时——每15分钟至少发送一个字节以保持静默流活跃。
  • waitUntil
    :15分钟
    。通过
    waitUntil
    注册的任务会在响应发送后保持调用活跃,最长15分钟——用于分析写入和审计日志等清理工作,而非后台任务运行器。(预览期间,
    @neon/functions
    中的
    waitUntil
    目前是存根。)
  • 空闲驱逐。无活跃连接时,Neon会关闭函数;也可能因操作原因(如维护,或将函数移动到不同的计算节点)驱逐/重启——活跃函数可先运行数小时。将驱逐视为进程重启——WebSocket/SSE客户端必须重新连接。Neon在驱逐前发送
    SIGINT
    ,因此
    process.on("SIGINT", ...)
    处理程序可让您检测到函数即将被驱逐并运行任何最后清理工作。您无需专门关闭Postgres连接——Neon的池化器会自行回收这些连接。
  • 运行时:Node.js 24,预览期间内存固定为2048 MiB。Slug必须匹配
    ^[a-z0-9]{1,20}$
    隔离实例会在多个请求间复用——同一隔离实例上可能同时有多个请求在处理(在Node的单线程事件循环上交错执行),负载下运行时会并行运行多个隔离实例,每个实例都有自己的模块状态副本。因此模块级状态是每个隔离实例独有的(由该隔离实例处理的所有请求共享),且仅在内存中——必须在Postgres中持久化任何需在驱逐后保留的数据。这种复用正是您在模块级创建一次连接池而非每个请求创建一次的原因(请查看连接Postgres)。

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
neon-ai-gateway
skill) for one credential across every model, with no extra provider keys. For a complete AI SDK agent running as a Function (streaming
toUIMessageStreamResponse
, 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.
The 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
    jwt
    plugin, NextAuth, or your own signer) — that call is fast and well within host limits.
  • Hand the token to the client and have it call the Neon Function directly (cross-origin), e.g. with the Vercel AI SDK:
    new DefaultChatTransport({ api: NEON_FUNCTION_URL, fetch })
    where
    fetch
    attaches
    Authorization: Bearer <token>
    . Your app server is never in the path of the long stream.
  • Add CORS so the browser can reach it (handle
    OPTIONS
    , set
    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
env
(see Environment Variables). Persist anything you need to keep (generated images, history) in Postgres — module state doesn't survive eviction.
Neon Function是AI Agent的理想宿主,正是因为它不会像Lambda风格无服务器那样超时(15分钟预算,请查看超时和运行时限制)。但一旦您通过Web应用后端代理Agent流(如Vercel、Netlify、Cloudflare等平台上的Next.js路由处理程序、Remix/SvelteKit/Nuxt操作等),这种优势就会消失。这些平台将无服务器/边缘执行限制在较短的窗口内(通常约10-60秒,有时最多约300秒),因此即使Neon Function可以持续运行,长期Agent或图像/视频生成流也会在响应中途被切断。
构建Agent本身。推荐使用Vercel AI SDKMastra构建Agent——将两者指向Neon AI网关(请查看
neon-ai-gateway
技能),即可通过一个凭据访问所有模型,无需额外的提供商密钥。如需完整的AI SDK Agent作为Function运行(流式
toUIMessageStreamResponse
、Postgres旁的多步工具调用、将生成的图像持久化到对象存储),请查看references/ai-sdk.md;如需带有内置追踪的Mastra等效方案,请查看references/mastra-studio.md
解决方案:从客户端直接调用函数。不要通过应用服务器路由长请求。
浏览器 ──(Authorization: Bearer <JWT>)──▶  Neon Function (agent)   ✅ 无宿主超时
浏览器 ──▶ 您的应用后端 ──▶ Neon Function                       ❌ 宿主切断流
  • 在应用后端生成短期JWT(如better-auth的
    jwt
    插件、NextAuth或您自己的签名器)——该调用速度快,完全在宿主限制内。
  • 将令牌交给客户端,让客户端直接(跨域)调用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) });
    }
  },
};
通过函数的
env
传递JWKS/颁发者URL(请查看环境变量)。在Postgres中持久化任何需要保留的内容(生成的图像、历史记录)——模块状态无法在驱逐后存活。

WebSocket 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
fetch
.
Call
upgradeWebSocket(request)
from
@neon/functions
and return the response it gives you. There is one entrypoint and no WebSocket dependency to install:
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;
  },
};
socket
is a standard
WebSocket
, so
addEventListener
and the
onopen
/
onmessage
/
onclose
/
onerror
properties both work. It is still
CONNECTING
when you get it — the runtime writes the
101
only once your handler returns
response
, and the socket opens then.
Three rules that matter:
  • Return
    response
    unchanged.
    A
    101
    can't be built as a plain
    Response
    (the fetch spec caps constructed responses at 200–599), so the runtime hands back an object carrying the pending upgrade.
    clone()
    , or rebuilding it with
    new Response(res.body, res)
    as response-rewriting middleware does, discards the upgrade and fails the request.
  • Refuse a handshake by returning an ordinary
    Response
    .
    A
    401
    ,
    403
    or
    404
    is relayed to the client as-is. That is how you gate a socket.
  • binaryType
    defaults to
    "arraybuffer"
    , not the browser's
    "blob"
    .
    event.data
    is a
    string
    for text frames and an
    ArrayBuffer
    for binary ones, so branch on
    typeof
    .
With auth. Browsers can't set headers on a WebSocket, so authenticate with a
?token=
query param (verify it the same way as the agent backend:
jwtVerify
against your JWKS) and refuse before upgrading:
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); // fan out to every isolate — see below
    });
    return response;
  },
};
Subprotocols. Pass
{ protocol }
to select one the client offered; it is echoed in
Sec-WebSocket-Protocol
and exposed as
socket.protocol
. Selecting one the client did not offer throws a
TypeError
. Omit it and no protocol is negotiated. No extensions are negotiated either —
socket.extensions
is always
""
and
permessage-deflate
is not available.
Hono. Nothing special is needed:
upgradeWebSocket
takes a
Request
, so call it inside a route with
c.req.raw
and return the response. Auth and everything else is ordinary middleware.
typescript
// 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分钟心跳,请查看超时)。
fetch
内部升级
。调用
@neon/functions
中的
upgradeWebSocket(request)
并返回它给出的响应。只有一个入口点,无需安装WebSocket依赖:
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;
  },
};
socket
是标准的
WebSocket
,因此
addEventListener
onopen
/
onmessage
/
onclose
/
onerror
属性都可用。获取它时仍处于
CONNECTING
状态——只有当您的处理程序返回
response
时,运行时才会写入
101
状态码,此时socket才会打开。
需要注意三个规则:
  • 原样返回
    response
    。无法通过普通
    Response
    构建
    101
    状态码(fetch规范将构造的响应限制在200-599之间),因此运行时会返回一个携带待升级状态的对象。
    clone()
    或像响应重写中间件那样用
    new Response(res.body, res)
    重建,会丢弃升级状态并导致请求失败。
  • 通过返回普通
    Response
    拒绝握手
    401
    403
    404
    会原样转发给客户端。这是您限制socket访问的方式。
  • binaryType
    默认为
    "arraybuffer"
    ,而非浏览器的
    "blob"
    event.data
    对于文本帧是
    string
    ,对于二进制帧是
    ArrayBuffer
    ,因此需根据
    typeof
    分支处理。
带身份验证。浏览器无法在WebSocket上设置标头,因此使用
?token=
查询参数进行身份验证(验证方式与Agent后端相同:针对您的JWKS进行
jwtVerify
),并在升级前拒绝无效请求:
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-Protocol
中回显,并作为
socket.protocol
暴露。选择客户端未提供的子协议会抛出
TypeError
。省略则不协商协议。也不协商扩展——
socket.extensions
始终为
""
,不支持
permessage-deflate
Hono集成。无需特殊操作:
upgradeWebSocket
接受
Request
,因此在路由中调用
c.req.raw
并返回响应即可。身份验证和其他所有操作都是普通中间件。
typescript
// 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
WebSocket
interface has no
ping()
, so send an application-level message the client ignores:
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永不静默。
标准
WebSocket
接口没有
ping()
方法,因此发送客户端忽略的应用级消息:
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
clients
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:
neon dev
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.
Module state doesn't survive eviction anyway, so Postgres is the shared source of truth. Pick a fan-out strategy. In every snippet below,
pool
is a pooled
pg
client and
clients
is this isolate's
Set
of live connections.
1. 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
    serial
    /
    bigserial
    PK and the interval sane.
  • Scale to Zero: ✅ preserved — polling stops when no clients are connected, so the compute suspends on its normal timer.
2.
LISTEN
/
NOTIFY
— lowest latency, but requires disabling Scale to Zero.
Each isolate
LISTEN
s on a channel over a dedicated unpooled connection; broadcasting is
NOTIFY
, 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).
typescript
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
LISTEN
/
NOTIFY
only on always-on compute that needs sub-second latency; move to Redis when fan-out outgrows Postgres.
负载下运行时会并行运行多个隔离实例,每个实例都有自己的模块状态副本——因此每个隔离实例都有自己的
clients
集合。仅向本地集合广播意味着隔离实例A上的客户端永远看不到隔离实例B上产生的事件,流会静默断裂。这很容易被忽略:
neon dev
运行单个进程(一个隔离实例),因此进程内广播在本地看起来总是正常的,但在生产环境中,并发连接会分散到多个隔离实例,从而导致问题。
模块状态无论如何都无法在驱逐后存活,因此Postgres是共享的事实来源。选择一种扩散策略。以下每个代码片段中,
pool
是池化的
pg
客户端,
clients
是当前隔离实例的
Set
活跃连接。
1. 轮询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.
LISTEN
/
NOTIFY
—— 延迟最低,但需禁用缩容至零
。每个隔离实例通过专用的非池化连接
LISTEN
一个通道;广播通过
NOTIFY
实现,因此每个隔离实例(包括发送者的隔离实例)都会将事件重新推送给自己的socket。几乎即时——但监听器保持的空闲连接不被视为活跃,因此缩容至零会暂停计算并断开连接,静默中断流。仅在始终运行的计算(禁用缩容至零——付费计划设置)上使用。
typescript
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或阻止缩容至零——代价是需要运行另一个服务。
经验法则:从轮询开始(支持缩容至零,无需额外基础设施);仅在需要亚秒级延迟的始终运行计算上切换到
LISTEN
/
NOTIFY
;当扩散规模超出Postgres能力时迁移到Redis。

Client 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
upgrade
auth check):
typescript
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 —
upgradeWebSocket
inside
fetch
, JWT auth over
?token=
, cross-isolate fan-out, and client backoff — these compose into a complete realtime chat backend on a single function.
空闲函数会被驱逐(且隔离实例会因操作原因重启),因此客户端的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();
结合
fetch
内部的
upgradeWebSocket
、通过
?token=
的JWT身份验证、跨隔离实例扩散和客户端退避,这些组件可组成一个完整的实时聊天后端,仅需一个函数。

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
fetch
handler returns a
Response
whose body is a
ReadableStream
with
Content-Type: text/event-stream
, and the runtime holds it open as long as bytes flow. The browser consumes it with
EventSource
, which reconnects on its own — so there's no client backoff to write.
typescript
// 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
: ping\n\n
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
Set
of stream controllers and
enqueue
to each).
EventSource
is GET-only and can't set headers, so authenticate with a
?token=
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.
当您仅需要服务器→客户端流式传输(实时计数器、通知、进度、令牌流)时,SSE比WebSocket更简单,且无需任何升级:普通
fetch
处理程序返回一个
Response
,其主体是带有
Content-Type: text/event-stream
ReadableStream
,运行时会在字节流动期间保持连接打开。浏览器使用
EventSource
消费它,它会自动重新连接——因此无需编写客户端退避逻辑。
typescript
// 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秒发送一条
: ping\ \ 
注释(如上所示),以防止空闲流被断开。将状态保存在Postgres中,并使用同步策略之一在隔离实例间扩散(持有
Set
的流控制器并向每个控制器
enqueue
)。
EventSource
仅支持GET,无法设置标头,因此使用
?token=
查询参数或cookie进行身份验证,与WebSocket场景完全相同。references/sse.md包含完整模式——Hono变体、跨隔离实例扩散、有线格式、客户端和注意事项。

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
POST
/
GET
on a single endpoint (conventionally
/mcp
), so it maps onto a function's
fetch
handler with no
upgrade
method or extra protocol.
The simplest host is a Hono app using the official
@modelcontextprotocol/sdk
plus
@hono/mcp
, which bridges the transport to a route. Build the server, register its tools, and create the transport once at module scope, then hand every
/mcp
request to it:
typescript
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 transportBetter 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
mcporter
/
add-mcp
.
MCP服务器是天然的Functions工作负载:长期运行的HTTP处理程序向AI客户端(Cursor、Claude、ChatGPT、Agent)公开工具,这些工具在计算资源旁读取和写入分支的Postgres。MCP的可流式HTTP传输是单个端点上的普通
POST
/
GET
(通常为
/mcp
),因此无需升级方法或额外协议即可映射到函数的
fetch
处理程序。
最简单的宿主是使用官方
@modelcontextprotocol/sdk
@hono/mcp
的Hono应用,后者将传输桥接到路由。构建服务器、注册工具并在模块级创建一次传输,然后将所有
/mcp
请求交给它:
typescript
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身份验证选项,以及使用
mcporter
/
add-mcp
进行测试。

Integrations and Observability

集成与可观测性

Built-in branch logs

内置分支日志

bash
neon logs query --branch production --source function --since 1h
Functions is one of the two sources branch logs cover today, alongside Object Storage. Logs are scoped to a single branch, so pass
--branch
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
neon
skill's Observability section.
bash
neon logs query --branch production --source function --since 1h
Functions是当前分支日志涵盖的两个来源之一,另一个是对象存储。日志范围限定为单个分支,因此当部署的函数不在您当前检出的分支上时,请传递
--branch
。关于日志的其他所有内容——所需的CLI版本、过滤器、SDK和兼容Loki的读取API——都在父级
neon
技能的可观测性部分。

Application 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
--env
or
neon.ts
env
.
  • 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可直接使用。在模块加载时初始化一次,通过环境变量控制,使本地开发和未配置的分支保持无操作状态,并通过
--env
neon.ts
env
传递密钥。
  • 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
.md
to the URL or by requesting
Accept: text/markdown
. Find the right page from the docs index (https://neon.com/docs/llms.txt) and the changelog announcements.
Neon文档是事实来源,且Functions正在快速演进,因此请始终对照官方文档进行验证。任何文档页面都可通过在URL后追加
.md
或请求
Accept: text/markdown
获取为markdown格式。从文档索引(https://neon.com/docs/llms.txt)和变更日志公告中找到正确的页面。

Further Reading

进一步阅读