Hyperdrive:接上你既有的 Postgres / MySQL
第 9 到 13 章講的是「用 Cloudflare 自己的儲存」。但大多數真實的遷移不是從零開始 —— 你已經有一套跑在 RDS 或 Supabase 上的 Postgres,裡面有五年的資料、幾十張表、以及一堆你不想重寫的 SQL。
Hyperdrive 的定位就是這個:讓 Workers 用你既有的關聯式資料庫,而不是要你把資料搬進 D1。
它做三件事(官方 how-it-works 頁):
- 「在你的 Workers 附近完成新資料庫連線的建立」
- 「在你的資料庫附近池化既有連線」
- 「快取查詢結果」
前兩件是純粹的加速,沒有語意風險。第三件會改變你程式的正確性,而且它預設是開的。本章一半的篇幅在講這件事。
28.1 為什麼需要它:連線的物理限制
Section titled “28.1 為什麼需要它:連線的物理限制”一個傳統的 Node.js 後端會維持一個連線池,程序啟動時建好,之後一直重用。Workers 沒有這個奢侈 —— 每個請求可能落在不同的 isolate、不同的 colo,而且第 3 章講過,跨請求持有 I/O 資源是不允許的。
沒有 Hyperdrive 的話,每個請求都要付一次完整的連線成本:TCP 三次握手 + TLS 握手 + Postgres 的認證交握。如果你的資料庫在 us-east-1 而請求來自東京,光是這幾個 round trip 就是 500 ms 起跳。更糟的是連線數 —— 十萬個並行請求就是十萬條連線請求打向一個最多接受幾百條連線的資料庫。
Hyperdrive 把連線池放在靠近資料庫的地方,把連線建立放在靠近 Worker 的地方,兩邊之間走 Cloudflare 自己的網路。
池化模式是 transaction mode(官方 connection-pooling 頁):
「執行查詢的 client 在一個 transaction 期間透過單一連線通訊」,然後「連線被歸還到 pool」。 「當連線歸還到 pool 時,連線會被
RESET,因此SET指令不會影響後續的查詢。」
這一句話推導出一個實務限制:任何依賴 session 狀態的東西都會壞掉。 SET search_path、SET TIME ZONE、advisory lock、LISTEN/NOTIFY、SQL 層級的 PREPARE —— 官方的 supported-features 頁把這些都列為不支援。如果你的既有程式碼在連線建立後跑一段 SET,遷移時要先處理它。
28.2 設定
Section titled “28.2 設定”{ "compatibility_flags": ["nodejs_compat"], "hyperdrive": [ { "binding": "HYPERDRIVE", "id": "<your config id>", "localConnectionString": "postgres://user:password@127.0.0.1:5433/linkforge" } ]}nodejs_compat 是必要的(get-started 頁:「加入 nodejs_compat compatibility flag,並把 compatibility date 設為 2024-09-23 或更晚」)。所有 Postgres / MySQL driver 都需要 node:net 與 node:tls。
查 config-schema.json,這個 binding 只有三個欄位、additionalProperties: false:
{ "properties": { "binding": {...}, "id": {...}, "localConnectionString": {...} }, "required": ["binding", "id"], "additionalProperties": false}沒有 remote 欄位。 Hyperdrive 不支援 remote binding(對照第 23 章的 Pipelines 有、第 22 章的 Analytics Engine 沒有)。要用 production 的 Hyperdrive 設定,走的是 wrangler dev --remote 這個旗標,不是 binding 屬性。
⚠️ Wrangler 的 configuration 參考頁只列出
binding與id,沒有列localConnectionString—— 那個欄位只出現在 Hyperdrive 的 local-development 頁。兩份官方文件的涵蓋範圍不一致。
localConnectionString 一定要有密碼
Section titled “localConnectionString 一定要有密碼”實測沒帶密碼的連線字串:
✘ [ERROR] Unexpected options passed to `new Miniflare()` constructor: hyperdrives: { HYPERDRIVE: 'postgres://postgres@127.0.0.1:5433/linkforge', ^ You must provide a password - e.g. 'user:password@database.example.com:port/databasename' }即使你的本機 Postgres 用 trust 認證不需要密碼,字串裡也必須有一個。
環境變數:舊名已被標記為 deprecated
Section titled “環境變數:舊名已被標記為 deprecated”除了寫進設定檔,也可以用環境變數。實測 wrangler 4.118.0 的原始碼(applyHyperdriveEnvVars):
const prefix = `CLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_`;const deprecatedPrefix = `WRANGLER_HYPERDRIVE_LOCAL_CONNECTION_STRING_`;現行是 CLOUDFLARE_ 開頭;WRANGLER_ 開頭是舊名,程式碼裡的變數名就叫 deprecatedPrefix。 兩個都還能用(會先找新的、再退回舊的),但 2025 年的教學寫的都是舊名。
同一段程式碼還告訴我們,兩者都沒設時的錯誤訊息:
When developing locally, you should use a local Postgres connection string to emulateHyperdrive functionality. Please setup Postgres locally and set the value of the'CLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_<BINDING>' variable or "<BINDING>"'s"localConnectionString" to the Postgres connection string.28.3 binding 的實際形狀
Section titled “28.3 binding 的實際形狀”範例的 /binding 路由對著一個真的本機 Postgres 16 執行。實測輸出:
{ "typeofBinding": "object", "ownKeys": ["connectionString", "port", "host", "password", "user", "database"], "protoKeys": ["connect", "constructor"], "ctorName": "Hyperdrive", "fields": { "host": "99df8f500677382d02858007fb2d785f.hyperdrive.local", "port": 5432, "user": "postgres", "database": "linkforge", "passwordLength": 7 }, "connectionStringShape": "postgres://<redacted>@99df8f500677382d02858007fb2d785f.hyperdrive.local:5432/linkforge?sslmode=disable"}四個觀察:
(一)host 不是你的資料庫位址。 我的 localConnectionString 指向 127.0.0.1:5433,但 binding 回報的是 99df8f50....hyperdrive.local:5432。即使在本機,wrangler 也插了一層 shim —— 你的 driver 連的是這個假 host,wrangler 再轉發到真的資料庫。這代表:你不能靠比對 host 來判斷「我現在連的是本機還是 production」。
(二)連線字串被加上 ?sslmode=disable。 本機如此;production 的設定由 --sslmode 決定。
(三)prototype 上有一個 connect 方法。 這是給原始 TCP socket 用的(第 4 章提過的 connect() API),大多數情況下你不會直接用它 —— driver 會處理。
(四)六個 own property 都在。 包含 connectionString 和五個離散欄位。
MySQL 為什麼所有範例都用離散欄位
Section titled “MySQL 為什麼所有範例都用離散欄位”官方所有 MySQL 範例都是這樣寫:
const connection = await createConnection({ host: env.HYPERDRIVE.host, user: env.HYPERDRIVE.user, password: env.HYPERDRIVE.password, database: env.HYPERDRIVE.database, port: env.HYPERDRIVE.port, disableEval: true, // 官方註解只寫「Required to enable mysql2 compatibility for Workers」});而 Postgres 的範例全部用 connectionString。
這裡本章必須比常見說法保守。 我看到很多教學(包括本系列的大綱初稿)說「MySQL 必須用離散欄位,不能用 connectionString」。我在 connect-to-MySQL、mysql2、mysql、supported-databases 幾頁上逐字搜尋
connectionString,一次都沒有出現。也就是說:官方是「只示範離散欄位」,不是「明文禁止 connectionString」。 這兩件事不一樣。照著官方範例寫離散欄位當然最安全,但本章不宣稱有一條不存在的禁令。
disableEval: true 的真正原因
Section titled “disableEval: true 的真正原因”官方文件寫了這個要求,但從頭到尾沒有解釋為什麼。我逐字搜尋過 eval、dynamic code generation、Workers runtime restriction,都沒有。
原因在第 27 章已經實測過了:
{ "threwName": "EvalError", "threw": "Code generation from strings disallowed for this context" }mysql2 為了效能,會用 eval() 動態編譯每張結果表的 row parser。Workers 禁止 eval,所以:
沒加
disableEval: true的話,createConnection()會成功,第一次query()才會炸。 失敗點在你意想不到的地方 —— 連線看起來是好的。
範例的 /mysql2 路由把這件事釘死:mysql2/promise 可以正常 import(createConnection 是 function),但 eval 在同一個 isolate 裡確實丟 EvalError。
28.4 兩個 driver,實測
Section titled “28.4 兩個 driver,實測”範例對著真的 Postgres 16 跑。
node-postgres (pg)
Section titled “node-postgres (pg)”import { Client } from "pg";
const client = new Client({ connectionString: env.HYPERDRIVE.connectionString });await client.connect();const res = await client.query("select slug, url from links where tenant_id = $1", ["acme"]);實測回傳 2 列,總耗時 23 ms(含冷啟)。
最低版本 8.16.3(官方明文)。另外有一個歷史地雷:8.11.4 引入了一個 URL 解析的 bug,「will not work」,8.11.5 修好。
postgres.js
Section titled “postgres.js”import postgres from "postgres";
const sql = postgres(env.HYPERDRIVE.connectionString, { max: 5, fetch_types: false, prepare: true,});實測 15 ms,比 pg 略快。最低版本 postgres@>3.4.5。
三個選項各有理由:
max: 5—— 官方註解:「因為 Workers 對並行外部連線的限制,把每個 Worker 請求的連線數限制在 5」。fetch_types: false—— 如果你的 schema 沒有 array type,這會省掉「一次額外的 round trip(不必要的延遲)」。prepare: true—— 這是預設值,而且是官方推薦。
prepare這一項要特別強調,因為它和大多數人的直覺相反。用過 pgbouncer 的人都學過「transaction pooling 模式下要關掉 prepared statement」。Hyperdrive 也是 transaction pooling,所以很多人直覺會寫
prepare: false。但官方 postgres.js 頁明說:「Hyperdrive will not cache prepared statements when this option is set to false.」 也就是關掉它反而讓你失去 Hyperdrive 的 prepared statement 快取。connect-to-postgres 頁還補充,Kysely 這類 query builder 會自己關掉 prepare,「requiring additional round-trips」。
在 Hyperdrive 上,
prepare: true。 這是 Hyperdrive 和 pgbouncer 行為不同的地方。
interactive transaction 可以用
Section titled “interactive transaction 可以用”這是 Hyperdrive 相對 D1 最大的差異。第 10 章實測過 D1 的 db.transaction() 會丟 Failed query: begin;Hyperdrive 沒有這個問題:
{ "insideTx": 1, "afterRollback": 0, "ms": 24 }begin → update → 交易內讀到 1 → rollback → 交易外讀回 0。完整的 interactive transaction 語意。
如果你的業務邏輯需要「讀取、根據結果決定、再寫入」的原子性,這就是選 Hyperdrive 而不是 D1 的理由。
/bench?n=4 實測(本機,所以是下限):
| 次數 | connect | query |
|---|---|---|
| 1 | 7 ms | 1 ms |
| 2 | 5 ms | 1 ms |
| 3 | 5 ms | 1 ms |
| 4 | 5 ms | 0 ms |
連線建立仍然是主要成本。production 上這個數字取決於 Hyperdrive 的 pool 有沒有命中。
連線生命週期:一個被淘汰的慣例
Section titled “連線生命週期:一個被淘汰的慣例”網路上(以及舊版 Hyperdrive 文件)到處都是這個寫法:
ctx.waitUntil(client.end()); // ← 已經不需要了現行的 connection-lifecycle 文件明說不需要:
「你不需要呼叫
client.end()、sql.end()、connection.end()(或類似方法)來清理資料庫 client。」 「Workers 到 Hyperdrive 的連線會在請求或 invocation 結束時自動清理,包含 Workflow 或 Queue consumer 完成時,或 Durable Object 進入 hibernation 時。」
同一頁確實要求的是位置:
「永遠在你的 request handler(
fetch、queue之類)內部建立資料庫 client,不要在全域 scope。」
全域 scope 的反面教材,官方直接寫在程式碼註解裡:
「🔴 Bad: Client created in global scope persists across requests. Workers do not allow I/O across request contexts, so this client becomes stale and subsequent queries will throw hard errors.」
這正是第 1 章與第 3 章那條規則的又一次體現。範例專案已經改成現行寫法 —— 在 handler 裡建立、不呼叫 end()。
(waitUntil(end()) 沒有被記載為有害,只是被記載為多餘。)
28.5 查詢快取:本章的正確性陷阱
Section titled “28.5 查詢快取:本章的正確性陷阱”| 設定 | 預設 | 上限 |
|---|---|---|
max_age | 60 秒 | 1 小時 |
stale_while_revalidate | 15 秒 | — |
| 快取回應大小 | — | 50 MB(超過不快取,但仍然回傳給你的 Worker) |
快取預設是開的。 官方 overview 的功能卡片:「Query Caching — Default-on caching for your most popular queries executed against your database.」
什麼會被快取
Section titled “什麼會被快取”「Hyperdrive 快取符合條件的唯讀查詢回應,不快取寫入。」 「Hyperdrive 使用資料庫協定來區分 mutating query(會寫入資料庫的查詢)與 non-mutating query(唯讀查詢)。」 「Mutating query(包含
INSERT、UPSERT、CREATE TABLE)以及使用被 PostgreSQL 標示為volatile或stable的函式的查詢不會被快取。」
不會被快取的函式例子:NOW()、RANDOM()、CURRENT_DATE、LASTVAL()。
寫入不會讓快取失效
Section titled “寫入不會讓快取失效”這是全章最重要的一句話,官方原文:
「Hyperdrive does not purge or invalidate cached read query results when your application writes to your database. A later matching
SELECTcan return the cached result until the configuredmax_ageexpires.」(Hyperdrive 在你的應用寫入資料庫時,不會清除或使快取的讀取結果失效。之後一個相符的
SELECT可以持續回傳快取結果,直到設定的max_age過期。)
沒有寫入失效機制。完全沒有。只有 TTL 過期。
具體後果:
await sql`insert into links (slug, url) values ('new', 'https://...')`;const rows = await sql`select * from links where tenant_id = 'acme'`;// ↑ 這一行可能回傳一份不含 'new' 的舊結果,最長 60 + 15 = 75 秒使用者建立了一個短網址,跳轉回列表頁,看不到它。重新整理十次還是看不到。一分鐘後突然出現了。
官方的解法:兩個設定
Section titled “官方的解法:兩個設定”# 一般讀取(快取開啟,預設)npx wrangler hyperdrive create linkforge-cached --connection-string="..."
# read-after-write 路徑(快取關閉)npx wrangler hyperdrive create linkforge-fresh --connection-string="..." --caching-disabled"hyperdrive": [ { "binding": "HYPERDRIVE", "id": "<cached id>" }, { "binding": "HYPERDRIVE_NOCACHE", "id": "<fresh id>" }]一個 Worker 可以綁多個 Hyperdrive 設定,由你的程式碼決定每一條路徑用哪一個。官方的分類建議:
- 快取設定:高流量、能容忍過期的讀取 —— 儀表板、商品目錄、公開列表。
- 關閉快取的設定:認證、權限、帳務。
再加上一條本章的建議:任何「使用者剛剛才寫入的東西」都走 no-cache 設定。
沒有 per-query 的繞過機制
Section titled “沒有 per-query 的繞過機制”我原本以為可以像 HTTP 那樣加一個 header 或註解來繞過快取。沒有這種 API。
而且官方有一節標題就叫 「Do not use SQL comments as cache controls」,內容剛好和大家的直覺相反:
「Hyperdrive 使用基於文字的 pattern matching 來偵測查詢中某些不可快取的函式。這可能包含 SQL 註解裡的函式名稱。」 「包含 volatile 或不可快取函式名稱的註解,例如
-- NOW()或-- RANDOM(),可能導致該查詢被視為不可快取。」 「然而,不含不可快取函式名稱的註解文字不會改變 cache key。」 「Hyperdrive 並未把 SQL 註解記載為一種 cache-control API,而且 parser 行為可能因資料庫引擎而異。」 「避免在查詢文字中的任何地方(包括註解)提到不可快取的函式名稱。」
換句話說:加 -- NOW() 這種註解確實會意外關掉快取,但官方明確表示這不是 API、不保證、不要依賴。
本機測不出來
Section titled “本機測不出來”範例的 /raw-test 路由做的是:寫入一列,然後分別用兩個 binding 讀回來。實測:
{ "slug": "probe-5429e1a7", "cached": { "ok": { "rowCount": 1 }, "ms": 10 }, "uncached": { "ok": { "rowCount": 1 }, "ms": 7 }}兩邊都讀到了。 因為本機根本沒有 Hyperdrive 的快取 —— 官方 local-development 頁:
「使用
localConnectionString時,Hyperdrive 的連線池化與查詢快取不會生效。」 「你的 Worker 直接連到資料庫,不經過 Hyperdrive。」
而且 /compare-bindings 顯示:binding 上沒有任何屬性告訴你快取是開還是關。那是建立設定時決定的伺服器端狀態,對你的程式碼完全不可見。
所以這一章的陷阱和第 27 章的 PBKDF2 是同一個形狀:本機看不到、production 才發生、而且沒有任何執行期訊號。
唯一能實測的方法是
wrangler dev --remote(官方:「在 Cloudflare 上執行你的 Worker,使用你已部署的 Hyperdrive 設定」、「這對測試啟用了 Hyperdrive 連線池化與查詢快取的情況很有用」)。開發 read-after-write 路徑時務必用它跑一次。
用 metrics 檢查快取行為
Section titled “用 metrics 檢查快取行為”官方提供兩個 GraphQL 資料集:hyperdriveQueriesAdaptiveGroups 與 hyperdrivePoolSizesAdaptiveGroups。
其中 cacheStatus 這個維度特別有價值,它會告訴你為什麼沒被快取:
disabled | hit | miss | uncacheable | multiplestatements | notaquery| oversizedquery | oversizedresult | parseerror | transaction | volatilevolatile(用了 NOW() 這類函式)、transaction(在交易裡)、oversizedresult(超過 50 MB)—— 這幾個值直接對應到你程式碼裡的具體問題。上線後第一件事就是看這個分佈。
沒有單一的 cacheHitRatio 欄位,要自己從 cacheStatus 分組算。
28.6 限制與計費
Section titled “28.6 限制與計費”限制(官方 limits 頁)
Section titled “限制(官方 limits 頁)”| 限制 | Free | Paid |
|---|---|---|
| 設定數 | 10 / 帳號 | 25 / 帳號 |
| 使用者名稱長度 | 63 bytes | 63 bytes |
| 資料庫名稱長度 | 63 bytes | 63 bytes |
| 初始連線 timeout | 15 秒 | 15 秒 |
| 閒置連線 timeout | 10 分鐘 | 10 分鐘 |
| 對來源資料庫的最大連線數(每個設定) | ~20 | ~100 |
| 單一查詢最長時間 | 60 秒 | 60 秒 |
| 快取回應大小上限 | 50 MB | 50 MB |
兩個註腳值得注意:
- 63 bytes 那條「是 PostgreSQL 強制的限制。某些資料庫供應商可能執行更小的限制。」
- 連線數那條:「Hyperdrive 是分散式系統,所以 client 可能無法連到既有的 pool。這種情況下會建立一個新的 pool,有自己的連線配額。」—— 所以「~100」是近似值,實際可能更多。
沒有記載的: 查詢大小上限、回傳列數上限。(我逐字搜尋確認過,這是缺席而非搜尋失敗。)
另外:「Hyperdrive 不限制來自 Workers 的並行 client 連線數。」
「Hyperdrive 包含在 Free 與 Paid 兩種 Workers 方案中。」
| 項目 | Free | Paid |
|---|---|---|
| 資料庫查詢 | 每日 100,000 次 | 無限 |
「Database queries 指透過 Hyperdrive 發出的任何資料庫敘述,不論是查詢(SELECT)、修改(INSERT、UPDATE、DELETE)或 schema 變更(CREATE、ALTER、DROP)。」
三個明確的 FAQ 答案:
- 連線池化或查詢快取會不會另外收費?「No.」
- 快取的查詢和沒快取的查詢計費一樣嗎?是的,「不論快取與否、不論查詢或變更,都依上述限制計算」。
- 資料傳輸/egress 收費嗎?「No.」
注意:「Hyperdrive 是免費的」這個說法已經過時。它不額外收費,但 Free 方案現在有每日 10 萬次查詢的上限。而且快取命中也計入這個額度 —— 快取省的是延遲和資料庫負載,不是查詢計數。
28.7 支援的資料庫
Section titled “28.7 支援的資料庫”PostgreSQL 9.0 到 17.x;MySQL 5.7 到 8.x。
| 引擎 | 支援 | 備註 |
|---|---|---|
| PostgreSQL / MySQL | ✅ | |
| AWS Aurora | ✅ | Postgres 相容與 MySQL 相容皆可 |
| Neon / Supabase | ✅ | 兩者目前跑 Postgres 15.x |
| Timescale / Materialize / CockroachDB | ✅ | Postgres 相容 |
| PlanetScale | ✅ | 提供 MySQL 相容與 PostgreSQL 資料庫 |
| MariaDB | ✅ | MySQL 相容 |
| SQL Server | ❌ | 「Not currently supported.」 |
| MongoDB | ❌ | 「Not currently supported.」 |
| D1 | ❌ | 「Hyperdrive 不支援 D1,因為 D1 的設計本來就提供從 Workers 的快速連線。」 |
還有一條:「Hyperdrive 不支援不安全的純文字連線。」 必須有 TLS。
不支援的功能(遷移前必看)
Section titled “不支援的功能(遷移前必看)”PostgreSQL: SQL 層級的 prepared statement 管理(PREPARE、DISCARD、DEALLOCATE、EXECUTE)、advisory lock、LISTEN/NOTIFY、per-session 狀態修改。
MySQL: 查詢中的非 UTF8 字元、USE 敘述、multi-statement 查詢、SQL 層級的 prepared statement、COM_INIT_DB 訊息、caching_sha2_password 與 mysql_native_password 以外的認證外掛。
LISTEN/NOTIFY 那條對很多既有系統是硬傷 —— 如果你用它做即時通知,遷移時要換成 Queues(第 19 章)或 Durable Object 的 WebSocket(第 16 章)。
28.8 私有資料庫
Section titled “28.8 私有資料庫”如果你的 Postgres 在 VPC 裡、沒有公開 IP,有兩條路:
(一)Workers VPC —— 官方標示「(Recommended)」。 用 --service-id 指定 Workers VPC Service ID。這是 2026-04 才加入的能力。
(二)Cloudflare Tunnel + Access。 較舊的路徑:
「Cloudflare Tunnel 用來建立安全的通道連線」;「Cloudflare Access 用來限制對通道的存取,使得只有特定的 Hyperdrive 設定能存取它。」 「Access application 必須設定一個 Policy,要求請求包含有效的 Service Auth token。」
一個很容易漏掉的細節,官方原文:
「建立私有資料庫的 Hyperdrive 設定時,你必須輸入
access-client-id與access-client-secret,並且省略port。」
省略 port —— 這一條不看仔細會卡很久。
28.9 LinkForge:從既有系統遷移
Section titled “28.9 LinkForge:從既有系統遷移”假設 LinkForge 原本跑在 RDS Postgres 上,現在要搬到 Workers 但暫時不動資料庫。
第一件事是把所有查詢路徑分成兩類:
| 路徑 | 特性 | binding |
|---|---|---|
GET /:slug 重導向 | 極高流量、短網址幾乎不變 | HYPERDRIVE(快取) |
GET /api/links 列表 | 使用者剛剛可能新增過 | HYPERDRIVE_NOCACHE |
POST /api/links 建立 | 寫入 | 任一(寫入本來就不快取) |
GET /api/me / 權限檢查 | 認證相關 | HYPERDRIVE_NOCACHE |
GET /api/stats/* 統計 | 容忍分鐘級延遲 | HYPERDRIVE(快取) |
import { Client } from "pg";
// 兩個 binding,兩個用途。命名要讓人一眼看出語意。export function freshDb(env: Env) { return new Client({ connectionString: env.HYPERDRIVE_NOCACHE.connectionString });}export function cachedDb(env: Env) { return new Client({ connectionString: env.HYPERDRIVE.connectionString });}建議把命名寫得很直白(freshDb / cachedDb 而不是 db1 / db2),因為 28.5 說過,執行期沒有任何訊號能告訴你這個 binding 到底有沒有快取。命名是唯一的文件。
重導向路徑:快取正好對味
Section titled “重導向路徑:快取正好對味”app.get("/:slug", async (c) => { const db = cachedDb(c.env); // 60 秒的快取,完全可以接受 await db.connect(); const r = await db.query<{ url: string }>( "select url from links where slug = $1", [c.req.param("slug")]); if (!r.rows[0]) return c.notFound();
// 點擊計數走 Queue(第 19 章),不要在熱路徑上寫資料庫 c.executionCtx.waitUntil(c.env.CLICKS.send({ slug: c.req.param("slug"), ts: Date.now() })); return c.redirect(r.rows[0].url, 302);});短網址的 target URL 幾乎不會變,60 秒的快取在這裡是純收益 —— 而且它擋掉了絕大部分打向資料庫的流量。
建立後立刻列表:必須走 no-cache
Section titled “建立後立刻列表:必須走 no-cache”app.post("/api/links", async (c) => { const db = freshDb(c.env); await db.connect(); await db.query("insert into links (slug, tenant_id, url) values ($1,$2,$3)", [...]); return c.json({ ok: true }, 201);});
app.get("/api/links", async (c) => { const db = freshDb(c.env); // ← 關鍵。用 cachedDb 會漏掉剛建立的那一筆 await db.connect(); const r = await db.query("select * from links where tenant_id = $1 order by created_at desc", [c.get("session").tenantId]); return c.json({ links: r.rows });});混合策略:Hyperdrive + D1
Section titled “混合策略:Hyperdrive + D1”遷移不必是全有全無。一個實務上很有效的組合:
- 既有的大表留在 Postgres,透過 Hyperdrive 存取 —— 不用改 schema、不用搬資料。
- 新功能用 D1(第 9 章)—— 例如點擊分析的彙總表。
- 熱路徑用 KV(第 8 章)—— 短網址解析可以完全不碰資料庫。
這樣你可以按自己的節奏遷移,而不是停機一個週末。
遷移前的檢查清單
Section titled “遷移前的檢查清單”- 有沒有用
LISTEN/NOTIFY?→ 換成 Queues 或 DO WebSocket。 - 有沒有連線後跑
SET?→ transaction pooling 會RESET掉。 - 有沒有 advisory lock?→ 換成 Durable Object(第 14 章的序列化保證)。
- 有沒有跑超過 60 秒的查詢?→ 拆開,或搬到別的地方跑。
- 有沒有 read-after-write 的頁面?→ 全部改走 no-cache binding。
mysql2加了disableEval: true嗎?pg版本 ≥ 8.16.3 嗎?- Free 方案的話,每日查詢會超過 10 萬次嗎?
28.10 本章實測結論彙整
Section titled “28.10 本章實測結論彙整”| # | 結論 | 影響 |
|---|---|---|
| 1 | binding 的 host 是合成的 *.hyperdrive.local,port 5432 —— 即使在本機也是 | 不能靠 host 判斷連的是哪裡 |
| 2 | binding 有 6 個 own property + prototype 上一個 connect 方法,ctorName 為 Hyperdrive | |
| 3 | 本機連線字串必須含密碼,否則 Miniflare 拒絕啟動 | 即使本機用 trust 認證 |
| 4 | 環境變數現行前綴是 CLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_;WRANGLER_ 前綴在 wrangler 原始碼裡就叫 deprecatedPrefix | 2025 年教學寫的都是舊名 |
| 5 | config schema 只有 binding / id / localConnectionString,additionalProperties: false,沒有 remote | 要測 production 設定得用 wrangler dev --remote 旗標 |
| 6 | Wrangler configuration 參考頁沒有列出 localConnectionString | 兩份官方文件涵蓋不一致 |
| 7 | 寫入不會使快取失效(官方原文引用於 28.5) | 最長 60+15 秒讀到舊資料 |
| 8 | 本機完全沒有快取(官方明載),實測兩個 binding 都讀到剛寫入的列 | 這個陷阱本機測不出來 |
| 9 | binding 上沒有任何屬性能分辨快取開關 | 只能靠命名紀律 |
| 10 | 沒有 per-query 的快取繞過 API;官方有一節專門叫「Do not use SQL comments as cache controls」 | -- NOW() 確實會意外關快取,但不保證 |
| 11 | interactive transaction 可用 —— 實測 begin/update/交易內讀/rollback 全部正確 | 這是相對 D1(第 10 章)最大的差異 |
| 12 | 現行文件說不需要呼叫 client.end(),清理是自動的 | ctx.waitUntil(client.end()) 是被淘汰的慣例 |
| 13 | 但必須在 handler 內建立 client;全域 scope 官方標為「🔴 Bad」 | 同第 1、3 章的規則 |
| 14 | prepare: true(預設)是官方推薦;設 false 會讓 Hyperdrive 不快取 prepared statement | 與 pgbouncer 的常識相反 |
| 15 | pg 最低 8.16.3;8.11.4 有 URL 解析 bug(8.11.5 修正);postgres.js 最低 3.4.5;mysql2 最低 3.13.0 | |
| 16 | 官方只示範 MySQL 用離散欄位,從未明文禁止 connectionString | 常見教學把「只示範」講成「禁止」 |
| 17 | 官方寫了 disableEval: true 是必需的,但從未解釋原因 | 原因是第 27 章實測的 EvalError |
| 18 | 沒加 disableEval 時,createConnection() 會成功,第一次 query 才炸 | 失敗點不直覺 |
| 19 | 連線池是 transaction mode,歸還時會 RESET | SET、advisory lock、LISTEN/NOTIFY 全部不可用 |
| 20 | Free 每日 100,000 次查詢;快取命中也計入 | 「Hyperdrive 免費」已是過時說法 |
| 21 | 設定數 Free 10 / Paid 25;連線數 ~20 / ~100(官方標明是近似值) | |
| 22 | 查詢大小與回傳列數沒有記載的上限 | 缺席而非搜尋失敗 |
| 23 | SQL Server、MongoDB、D1 明確不支援;不接受純文字連線 | |
| 24 | 私有資料庫官方推薦 Workers VPC(--service-id)而非 Tunnel + Access | Tunnel 路徑要記得省略 port |
| 25 | hyperdriveQueriesAdaptiveGroups 的 cacheStatus 有 11 個值,能指出沒被快取的原因 | 沒有單一 cacheHitRatio 欄位 |
28.11 動手練習
Section titled “28.11 動手練習”- 用
wrangler dev --remote跑/raw-test,觀察cached這一路真的讀不到剛寫入的列 —— 這是本章唯一離線環境驗證不了的一半。 - 故意在一個
SELECT裡加上-- NOW()註解,用 metrics 的cacheStatus確認它變成volatile,然後想想為什麼官方不建議依賴這件事。 - 把
postgres.js的prepare從true改成false,比較cacheStatus分佈與queryLatency。 - 用
pg連線後執行SET search_path TO myschema,再發第二個查詢,確認 28.1 講的RESET行為。 - 對同一份資料,寫兩個版本的統計查詢:一個走 Hyperdrive + Postgres,一個走 D1(第 9 章),比較延遲與
rows_read計費。
- Hyperdrive overview · Get started · How Hyperdrive works
- Connection pooling · Connection lifecycle · Query caching
- Local development · Tune connection pool · Private DB via Workers VPC · Private DB via Tunnel
- Connect to PostgreSQL · node-postgres · Postgres.js · Connect to MySQL
- Limits · Pricing · Supported databases and features · Wrangler commands · Metrics
- 設定 schema 與環境變數行為來源:
node_modules/wrangler(4.118.0)的config-schema.json與wrangler-dist/cli.js
下一章(第 29 章):Containers on Workers(2026-04 GA)—— 當你的工作負載真的需要一個完整的 Linux 環境(ffmpeg、headless 瀏覽器、既有的 Python 服務)時,Workers 怎麼把它叫起來。