Cloudflare Workers + Supabase + Hyperdrive 最佳实践(2026):连接池与低延迟查询
2026-07 更新:这篇文章首发后,成了我被问得最多的一篇。这次更新我把它彻底加深了一轮:补上了「为什么 Workers 直连 Postgres 这么痛」的原理分析、Hyperdrive 配置全流程(wrangler.jsonc 绑定、连接串怎么选)、缓存行为的细节、性能提升的量化方法,以及一份可以直接对照排查的踩坑清单。原来的三关闯关记全部保留,新内容穿插其中。
再见 Vercel,你好 Cloudflare Worker。
前些天,一位朋友找到我,因为 Vercel 太贵,她想把一个网站迁移到 Cloudflare。其它问题似乎 AI 和她请的程序员都搞定了,但是由于之前使用的是 Supabase 数据库,而 Cloudflare Worker 不支持 Supabase,所以她咨询我应该迁移到什么数据库。
我的第一反应是不可能,绝对不可能。这都 2026 年,这么出名的 Supabase,Cloudflare Worker 不可能不支持。搞不定就丢给我,我分分钟拿下。
实际上,为了让她的 3D 模型处理服务 成功跑在 Cloudflare Worker 上,我经历了三场漫长的 Debug 战役,把整个数据库连接层重写了 N 遍。
如果你也正打算使用 Cloudflare Worker + Supabase,也在使用 AI 开发,那你也可能会踩进 Cloudflare Worker + Hyperdrive + Supabase 的坑。我建议你认真阅读这篇文章,然后把地址保存下来,将来开发的时候,把它丢给 AI,应该可以帮你节省很多时间。
为什么要逃离 Vercel?
Vercel 很好,Developer Experience 极佳。但是当你的用户量开始增长,你会发现 Vercel 的账单成长得更快……你此刻可能还没开始挣钱,但 Vercel 不管那些,看着每个月几十上百的账单,你会不由得焦虑起来……
Cloudflare Workers 提供了极高的性价比和几乎无限的并发能力,区区 $5/m,媲美 Vercel $100/m 以上的额度;还有 Hyperdrive —— 那个声称能让你的数据库连接像开了加速器一样的神奇功能。整个迁移过程(OpenNext 适配、构建配置、部署流水线)我在 在 Cloudflare Workers 上部署 Next.js 最佳实践 里写得很细,本文只聚焦数据库这一层。
是否应该选择 Supabase?
如果你只是需要一个数据库,我其实不太推荐 Supabase。相对来说,Supabase 是个非常强力的一站式解决方案,除了 Serverless 数据库,它还支持 Auth、Storage 和 Edge Function,可以用一个工具满足很多需求,尤其是开发移动 App 的时候,非常方便。
但如果你并不了解 Serverless 数据库应该怎么开发应用,或者只是需要一个关系型数据库,那么 Supabase 提供的好处你恐怕享受不到,而它的免费额度要远低于 D1 或者 TiDB Cloud。所以我起初的想法是,搞不定我就帮朋友迁移到 TiDB。后来也是上头了才开始跟 Hyperdrive + Supabase 死磕。
顺带一提,如果你选了 Cloudflare 全家桶,文件/对象存储也不必留在 Supabase Storage 上:R2 兼容 S3 协议、出站流量免费,跟 Workers 的配合更顺滑。具体接入方式可以看我这篇 用 Cloudflare R2 上传文件实战。
Workers 直连 Postgres,为什么这么痛?
在讲配置之前,先把原理讲透。理解了「痛在哪」,后面每一个反直觉的设定你都能自己推导出来。
每个请求都要重新握手
传统 Node.js 服务是长驻进程:启动时建好连接池,之后所有请求复用现成的连接,建连成本被摊薄到几乎为零。而 Workers 是彻底的 Serverless——实例随时创建、随时销毁,没有任何「常驻连接」可言。这意味着每个冷的数据库请求都要完整走一遍:
- TCP 三次握手
- TLS 握手(云数据库必然开着 SSL)
- Postgres 协议的认证流程(startup message、SASL/SCRAM 密码校验等,本身又是好几次往返)
- 最后才轮到你那条真正的查询
这一套下来是多次网络往返(RTT)。如果你的 Worker 跑在美西边缘节点,数据库在新加坡,一次 RTT 就是 150ms+,乘上握手需要的往返次数,一条 5ms 就能执行完的查询,光建连就烧掉大几百毫秒。这不是查询慢,是「路上」慢。
连接数上限:Serverless 的天敌
更致命的是连接数。Postgres 的每个连接都是真实的服务端资源,云数据库都会限制最大连接数(Supabase 免费档的直连数很有限,具体额度以官方文档为准)。而 Workers 的并发模型是「流量来了瞬间起几千个实例」,每个实例都天真地去开自己的连接,数据库瞬间就会被打爆,抛出经典的 Too many connections,然后所有人一起 503。
Hyperdrive 到底解决了什么
Hyperdrive 的思路是把上面两个问题一起端掉:
- 连接池驻留在 Cloudflare 网络里:Hyperdrive 在靠近你数据库的位置维护一个到真实数据库的长连接池,TCP/TLS/认证握手只在池子补充连接时发生。你的 Worker 连的是 Cloudflare 内网里的 Hyperdrive 接入点,几乎是内网延迟。
- Query Cache:对可缓存的读查询,Hyperdrive 可以直接在边缘返回缓存结果,连数据库都不用碰(后文详述缓存规则)。
对 Worker 侧来说,你拿到的仍然是一个标准的 Postgres 连接串,驱动层(postgres.js、node-postgres、Drizzle)完全无感,这是它最优雅的地方。
Hyperdrive 配置全流程
第一步:创建 Hyperdrive 配置
可以在 Dashboard 里点,也可以用 wrangler 一条命令搞定:
npx wrangler hyperdrive create my-supabase
--connection-string="postgres://postgres:你的密码@db.xxxxxxxx.supabase.co:5432/postgres"
注意两个细节:
- 这里填的是 Supabase 的直连地址(端口 5432),不是 pooler 地址,原因下面细说。
- 如果密码里有
@、#、?、/这类特殊字符,必须先做 URL 编码(encodeURIComponent的结果),否则连接串会在奇怪的位置被截断,报出来的错误千奇百怪,完全指不到根因。Supabase 随机生成的密码里出现特殊字符的概率不低,这个坑我替你踩过了。
第二步:wrangler.jsonc 里绑定
创建成功后会得到一个配置 ID,把它绑进 wrangler.jsonc:
{
"name": "app-worker",
"main": ".open-next/worker.js",
"compatibility_flags": ["nodejs_compat"],
"hyperdrive": [
{
"binding": "HYPERDRIVE",
"id": "这里填 wrangler 返回的配置 ID",
"localConnectionString": "postgres://postgres.xxxxxxxx:密码@aws-0-us-east-1.pooler.supabase.com:6543/postgres"
}
]
}
localConnectionString 是给本地开发用的:wrangler dev / opennextjs-cloudflare preview 跑在你本机,够不到 Cloudflare 内网的 Hyperdrive,所以本地会直接用这个连接串连数据库。也可以不写进配置文件,用环境变量 WRANGLER_HYPERDRIVE_LOCAL_CONNECTION_STRING_<绑定名> 传入,避免把连接串提交进仓库(具体变量名规则以官方文档为准)。
第三步:连接串怎么选——直连还是 Supavisor?
Supabase 会给你三种连接串,这是最容易选错的地方:
- Direct Connection(
db.xxx.supabase.co:5432):直连数据库本体。Hyperdrive 必须用这个。注意免费档的直连地址只解析到 IPv6。 - Supavisor Session Mode(pooler 域名,端口 5432):每个客户端独占一条池化连接,行为最接近直连。
- Supavisor Transaction Mode(pooler 域名,端口 6543):事务级复用,容量最大,但不支持部分会话级特性。本地开发用这个。
取舍逻辑其实一句话:Hyperdrive 自己就是 pooler,pooler 不该套 pooler——给 Hyperdrive 配直连;而你的本机连不上免费档的 IPv6 直连地址,所以本地开发反过来用 Supavisor 的 pooler 地址。两个环境用的连接串就应该不一样,这不是 bug,是架构决定的。我当初没想通这一点,来回折腾了很久。
Prepared Statements 兼容性
还有一个隐蔽的差异:postgres.js 默认会用 prepared statements。走 Hyperdrive(直连语义)没问题;但本地走 Supavisor 的 Transaction Mode(6543)时,prepared statements 的支持是受限的,可能出现「本地随机报错、线上完全正常」的灵异现象。保险做法是本地连 pooler 时显式关掉:
const client = postgres(connectionString, {
// 走 Supavisor transaction pooler 时关闭 prepared statements
prepare: source === 'hyperdrive' ? true : false,
});
Supavisor 对 prepared statements 的支持一直在改进,你读到本文时或许已经放开,具体以 Supabase 官方文档为准。但显式声明总归不吃亏。
第一关:消失的环境变量
关于环境变量,我写过一篇更详尽的文章,建议你阅读:Cloudflare Worker + Next.js 使用环境变量最佳实践(2026终极版)
在 Vercel(或者标准的 Node.js 环境)里,我们习惯了这样拿数据库连接串:
import postgres from 'postgres';
const connectionString = process.env.DATABASE_URL;
const client = postgres(connectionString)
export const db = drizzle(client, config);
代码写完,本地 next dev 一跑,完美。部署到 Cloudflare Worker,报错……
原因:Cloudflare Workers 的环境变量机制和 Node.js 不同。
在 Workers 运行时里,Hyperdrive 是 binding,并不是典型的环境变量,自然也不会挂在 process.env 上,而是通过 env 对象传递给请求的上下文。特别是当你使用了 @opennextjs/cloudflare 适配器时,你需要显式地去获取上下文。
并不是简单的 process.env
你需要把所有直接读取 process.env 的代码,改成通过 getCloudflareContext() 获取:
import { getCloudflareContext } from '@opennextjs/cloudflare';
// 必须在函数内部调用,因为只有真正在处理请求时才有 Context
function getConnectionInfo() {
const runtimeEnv = getCloudflareContext().env;
// 现在的 runtimeEnv 里才真正包含了你在 Cloudflare Dashboard 或者 wrangler.jsonc 配置的 vars 和 secrets,以及 bindings
// 然后再把 hyperdrive 拿出来创建实例
const hyperdrive = runtimeEnv.HYPERDRIVE as { connectionString?: string } | undefined;
if (hyperdrive?.connectionString) {
return { connectionString: hyperdrive.connectionString, source: 'hyperdrive' };
}
const localHyperdrive = runtimeEnv.CLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_HYPERDRIVE;
if (localHyperdrive) {
return { connectionString: localHyperdrive, source: 'hyperdrive-local' };
}
throw new Error('HYPERDRIVE is not configured');
}
这带来了一个巨大的架构变动:你不能在文件顶层(Top Level)初始化数据库连接了。
之前我们可以:
// global.ts
export const db = drizzle(client); // 全局单例,直接导出
现在如果你在顶层调用 getCloudflareContext(),它会抛错或者返回 undefined,因为模块加载时还没有请求进来。
第二关:Hyperdrive 真正的奥义:连接池
解决了环境变量,下一步就是连接数据库。
Cloudflare Hyperdrive 不仅可以加速全球访问,速度更快;实际上它对于 Serverless 架构来说,更核心的作用是维护 连接池(Connection Pooling)。前面原理部分已经讲过:Worker 不直接连真实的数据库,而是连 Cloudflare 部署在网络边缘的 Hyperdrive 代理,代理维护着到真实数据库的长连接池。你的 Worker 连 Hyperdrive 极快(内网级别),而 Hyperdrive 帮你把成千上万的 Worker 实例复用在有限的几十个真实数据库连接上。
这样一来,即使你有很多个应用,也不用考虑应用间的连接池共享,因为 Cloudflare 都帮你维护着。
Supabase 的坑中坑
朋友的数据库是 Supabase,Supabase 为了不要动不动就被阻塞,自己也提供连接池(Supavisor),而且默认引导用户使用连接池(连接 URL 中包含 pooler 字眼,使用 6543 端口)。
直觉告诉我:既然要连接池,那我用 Hyperdrive 连 Supabase 的 Pooler 岂不是双倍快乐?
现实告诉我:报错。
Hyperdrive 必须连接数据库的 Direct Connection(通常是 port 5432)。它自己就是一个 Pooler,它不适合去连另一个 Pooler(协议层面的会话语义对不上)。
这引出了三个连锁反应的坑:
- 必须用直连 Direct Connection:配置 Hyperdrive 时,Host 必须是 Supabase 的直连地址(特征:端口 5432)。
- 本地开发连不上:Supabase 的免费版(Free Tier)直连地址只支持 IPv6。如果你的本地网络环境或者开发工具只支持 IPv4,你在本地是死活连不上这个 Direct 地址的。对我来说,由于连不上,我以为这个连接 URL 有问题,直接放弃使用它,换用 6543 pooler,导致后面一系列反复的问题。
- 必须关闭 SSL:这里也很反直觉。通常连接云数据库必须开 SSL (
ssl: 'require')。但是,Worker 到 Hyperdrive 这一段走的是 Cloudflare 内部网络,驱动层必须显式关闭 SSL(Hyperdrive 到源数据库那一段的加密由 Hyperdrive 配置负责):
// 这里 cache 的作用下一节会解释
export const getDb = cache(() => {
const { connectionString, source } = getConnectionInfo();
return createDatabase({
connectionString,
// Worker 到 Hyperdrive 相当于内网,没有 SSL,必须关掉!
// 只有直连真实数据库(比如本地开发、build 阶段)才启用 require
enableSSL: source === 'hyperdrive' ? false : 'require',
});
});
我们在 DEBUG 期间被这个问题折磨了很久:明明连接串是对的,明明 Supabase 设置也没问题,但就是握手失败。尤其使用 AI 开发一定要小心,因为 AI 很可能会顺手就加上 SSL。
第三关:每个连接用一次,用完即弃
debug 过程中我发现,如果按照惯例缓存数据库连接实例反复使用,就会遇到这个错误:
The Workers runtime canceled this request because it detected that your Worker's code had hung and would never generate a response. Refer to: https://developers.cloudflare.com/workers/observability/errors/
表现为没有响应,到达设置的时限之后报超时错误。
后来认真阅读 OpenNext 官方文档 Cloudflare > How-Tos > Database & ORM 后,了解到不能全局缓存数据库连接实例,必须每个请求——这里的请求指 用户的请求,即一次用户访问产生的数据库连接请求,比如同一个 API 里执行 3 次查询,可以复用同一个连接——都需要创建新的连接实例才行。
具体到代码上,是这样的。在 Vercel/Node 时代,为了防止 serverless function 每次反复连接数据库把连接池撑爆,通常来说我们会使用全局单例:
// Before: Global Singleton
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
// 无论 import 多少次,client 和 db 只有一份
const client = postgres(process.env.DATABASE_URL!);
export const db = drizzle(client);
然后你在业务代码里随心所欲地 import { db } from '@/lib/db'。
但是,结合前面提到的两点:
- 必须在请求上下文 (
getCloudflareContext) 里才能拿到真实连接串。 - Hyperdrive 的连接建议是“随用随连”(轻量级)。
我们不得不把 db 从一个全局变量变成一个请求级函数。
// After: Function based
// 使用 React Cache 确保同一个请求内只创建一次以复用
// 这里可以是 async function,主要看是否在 Server component 里使用
export const getDb = cache(() => {
const { connectionString, source } = getConnectionInfo();
// 创建连接
return createDatabase({
connectionString,
// Hyperdrive 不需要 SSL,这里根据本地开发或线上部署切换 'require'
enableSSL: source === 'hyperdrive' ? false : 'require',
});
});
用 ctx.waitUntil 优雅收尾
「请求级创建」还有配套的后半句:请求结束后要把连接还回去。直接 await client.end() 会白白拖慢响应,正确姿势是交给 ctx.waitUntil,让运行时在响应发出后异步收尾。一个完整的 Route Handler 长这样(postgres.js 版):
import { getCloudflareContext } from '@opennextjs/cloudflare';
import postgres from 'postgres';
export async function GET() {
const { env, ctx } = getCloudflareContext();
const sql = postgres(env.HYPERDRIVE.connectionString, {
max: 5,
// 减少一次类型探测往返,Cloudflare 官方文档推荐
fetch_types: false,
});
const users = await sql`SELECT id, name FROM users LIMIT 10`;
// 响应返回后异步关闭连接,不阻塞响应本身
ctx.waitUntil(sql.end());
return Response.json(users);
}
用 node-postgres(pg)的话思路完全一样:请求内 new Client() + connect(),查询完 ctx.waitUntil(client.end())。别忘了 wrangler.jsonc 里要开 nodejs_compat。
有点痛苦的重构过程
这一改,意味着整个项目里几百个 import { db } from '@/lib/db' 全部失效。这是需要最大规模重构的部分,不过因为有 AI 存在,所以其实还好。
所有的 Server Action、API Route、Data Access Layer 全部要改:
Before:
import { db } from '@/lib/db';
export async function getUser(id: string) {
return await db.query.users.findFirst({ ... });
}
After:
import { getDb } from '@/lib/db';
export async function getUser(id: string) {
const db = await getDb(); // <--- 每一处都要加这个
return await db.query.users.findFirst({ ... });
}
不仅仅是加一行代码,由于 db 现在是异步获取或者在函数内部获取,很多原本直接导出 query 对象的工具函数也得重写。
缓存行为:哪些查询会被缓存?
Hyperdrive 的 Query Cache 默认开启,但它非常保守,只缓存「确定安全」的查询。理解规则后你就不用担心读到脏数据:
- 会被缓存的:不修改数据的读查询,典型的就是普通
SELECT。命中缓存时直接从边缘返回,连数据库都不碰。 - 不会被缓存的:所有写操作(
INSERT/UPDATE/DELETE)、事务内的查询、含易变函数(比如now()、random()这类每次结果都不同的函数)的查询——这些永远直达数据库。
换句话说:写路径的正确性 Hyperdrive 不会动,读路径的时效性你要自己权衡。缓存的默认过期时间不长,绝大多数「列表页、详情页」类查询完全无感;但如果你的业务是「写完立刻读、且必须读到最新值」(比如扣库存后立刻校验),要么把这段逻辑放进事务,要么为这类场景单独建一个关闭缓存的 Hyperdrive 配置——同一个数据库可以建多个 Hyperdrive 配置,各自独立设置缓存开关,缓存 TTL 的具体默认值以官方文档为准。
性能到底能提升多少?
很多文章只会说「快了很多」,我给你一个能自己复现的分析框架。Hyperdrive 省掉的核心开销是建连的多次 RTT:假设 Worker 边缘节点到数据库单程 100ms,TCP + TLS + Postgres 认证合计要好几次往返,每个冷请求就是固定几百毫秒的「入场费」。Hyperdrive 把这笔入场费变成了池内复用,一次付清、长期摊销。要量化的话,方法也简单:写一个只执行 SELECT 1 的接口,分别用直连和 Hyperdrive 各压几十次,对比 P50/P95——差值几乎全部来自建连开销。
「连接建立开销」这个问题有多普遍?我自己的博客(WordPress + MySQL,跟本文完全不同的技术栈)也中过同一枪:wp-json 接口一度慢到 1.3s,查了半天发现是 PHP 每个请求都在重新建立数据库连接。给 DB_HOST 加上 p: 前缀启用持久连接后,响应时间直接从 1.3s 降到 0.3s——什么查询优化都没做,只是不再反复握手。技术栈不同,病根相同:数据库慢的锅,经常是建连在背。Hyperdrive 就是把这个优化以托管服务的形式送到你手上。
再叠加 Query Cache:热门读查询直接边缘返回,连那一次池内往返都省了。实际收益取决于你的用户、边缘节点、数据库三者的地理关系——数据库离用户越远,Hyperdrive 的收益越夸张。
常见坑清单(Checklist)
把前面散落的坑集中列一遍,部署前逐条对照:
- 本地 dev 连不上 / 行为不一致:本地够不到 Hyperdrive,记得配
localConnectionString(或对应环境变量),并且本地用 Supavisor pooler 地址而不是 IPv6 直连地址。 - 免费版限制:Workers 免费计划也能用 Hyperdrive,但配置数量、缓存等能力与付费计划有差异;Supabase 免费档直连仅 IPv6、连接数有限。两边的具体额度都以官方文档为准,别按老博文的数字做容量规划。
- 连接串特殊字符:密码含
@、#、/等字符必须 URL 编码,否则报错信息会把你带偏到天涯海角。 - SSL 配置:走 Hyperdrive 时驱动层关 SSL,直连真实数据库时开
require,用source标记区分,别让 AI 顺手统一加上。 - Supabase RLS 与连接池的 auth 上下文:这是最隐蔽的一个。RLS 策略依赖
auth.uid()之类的会话上下文,而这套机制是配合 Supabase 客户端 SDK(走 PostgREST,每个请求带 JWT)设计的。当你通过 Hyperdrive + Postgres 驱动直连时,用的是postgres超级用户角色,RLS 对它并不生效,会话里也没有用户身份。所以权限校验必须在你的应用层(Server Action / API Route)自己做,别指望 RLS 兜底;反过来,如果你的表启用了 RLS 而查询「莫名少数据」,先检查连接用的是什么角色。 - 全局缓存连接实例:会触发 Worker 假死超时,老老实实请求级创建 +
ctx.waitUntil收尾。
总结
虽然过程很痛苦,但问题终归是解决了。以下是使用 Cloudflare Worker + Supabase 总攻略:
- 用 Supabase 直连 URL(端口 5432)创建 Hyperdrive 配置,在
wrangler.jsonc里绑定 - 本地开发使用 Supabase 的 Supavisor pooler URL(端口 6543),通过
localConnectionString配置,注意 prepared statements 兼容性 - 修改环境变量读取方式,妥善利用
getCloudflareContext().env,不要在模块顶层初始化连接 - 把全局
db单例重构成await getDb(),请求级创建连接,用ctx.waitUntil异步关闭 - 走 Hyperdrive 时关闭驱动层 SSL;权限校验放在应用层,别依赖 RLS
最后,我还是帮朋友把代码仓库修好了。Cloudflare Workers 的冷启动几乎可以忽略不计,配合 Hyperdrive,数据库查询速度在边缘节点也相当可观。最重要的是,再也不用担心 Next.js 部署在 Vercel 上的高昂账单了。
如果你也在做类似的迁移,希望上面的经验总结能帮你一次性解决问题。
常见问题(FAQ)
为什么在 Workers 里要用 Hyperdrive 连 Supabase?
Workers 无常驻连接,直连 Postgres 每个请求都要付出 TCP/TLS/认证握手的多次往返,还会撞上连接数上限;Hyperdrive 在 Cloudflare 网络内维护常驻连接池并缓存可缓存的读查询,显著降低查询延迟、避免连接耗尽。
Hyperdrive 支持 Supabase 的连接方式吗?
支持。用 Supabase 的直连地址(端口 5432)配置 Hyperdrive,即可在 Workers 中通过 Hyperdrive 绑定访问;Supavisor pooler 地址(端口 6543)留给本地开发用,Hyperdrive 本身就是连接池,不要给它再套一层 pooler。
Hyperdrive 会缓存写操作吗?
不会。Hyperdrive 只缓存可缓存的读查询,写操作、事务以及含易变函数的查询始终直达数据库,因此不必担心数据一致性问题。
延伸阅读
相关文章
Next.js 在 Cloudflare Workers 上生成 OG 图:Satori、缓存与 2026 预热实践
在 Cloudflare Workers 上为 Next.js 生成 Open Graph 图片:Satori/resvg 限制、冷启动与 CPU 时间、R2/CDN 缓存与发布时预热,附可复制的 r
TiDB 账单爆炸之后:一次跌宕起伏的降本排查,从 Cloudflare 边缘到数据库计费内核,降本$30/月
博客的 TiDB 账单逼近限额,RU 基线常年 110+。我用两天逐层排查:边缘缓存、漏洞扫描器、WordPress 对象缓存、连接税、TiFlash 副本、演示数据……一个接一个假设被数据打脸。把能
Cloudflare Email Worker 踩坑实录:三个你一定会遇到的问题
记录在用 Cloudflare Email Worker 处理邮件时遇到的三个常见坑:不能转发到同一 Worker、目标地址需验证、以及 message.raw 只能读一次,并给出用 R2 缓存 .e


