跳到內容

Pipelines 與 R2 SQL:把事件落地成資料湖

查證日期
驗證環境wrangler@4.118.0·workerd@1.20260730.1·compatibility_date: 2026-07-24

⚠️ Pipelines、R2 SQL、R2 Data Catalog 三者目前都是 open beta,且官方文件有多處互相矛盾。本章會逐一標明哪些是實測、哪些是文件、哪些是文件沒說的。查證日期 2026-08-01

第 22 章結束在一個明確的缺口上:Analytics Engine 會抽樣,而且只留 3 個月。它能回答「上週前 10 大來源國家」,但回答不了「這個 API key 昨天 14:32 做了什麼」,也回答不了「今年 Q1 對比去年 Q1」。

LinkForge 若要往 B2B SaaS 走,這兩個問題遲早會變成合約義務 —— 稽核軌跡與長期報表。這需要的是把每一筆事件原封不動落地,然後用一個能掃描 TB 級資料的引擎去查。這就是 Cloudflare Data Platform 的位置。

它由三個獨立產品組成,可以分開用也可以串起來用:

  • Pipelines —— 把事件流從 Worker 或 HTTP 端點,經過 SQL 轉換,寫進 R2。
  • R2 Data Catalog —— 內建在 R2 bucket 上的 Apache Iceberg 目錄,讓 Spark / DuckDB / Trino 這些引擎能直接讀。
  • R2 SQL —— Cloudflare 自家的 serverless 查詢引擎,直接查 R2 Data Catalog 裡的 Iceberg 表。

23.1 先講清楚:這一章的內容有多穩定

Section titled “23.1 先講清楚:這一章的內容有多穩定”

這是本系列第一次整章都在講 beta 產品,所以先把風險攤開。

(一)2025-09-25 有一次架構重寫。 舊的 Pipelines 是「一個 pipeline 物件 + --r2-bucket 參數」;新的是 Streams / Pipelines / Sinks 三物件模型。2025-09-25 之前建立的 pipeline 被官方稱為 legacy,「會持續運作到 Pipelines GA 為止」,但「新功能只會出現在新架構上」。官方沒有給 EOL 日期。

實測 wrangler pipelines --help(4.118.0),舊模型的痕跡還在:

wrangler pipelines update <pipeline> Update a pipeline configuration (legacy pipelines only) [open beta]

每一個子指令都標著 [open beta]

(二)Worker binding 的鍵名在 2026-05 改過名。 這是本章最容易踩的一個坑,23.3 詳述。

(三)文件與文件之間互相矛盾。 至少三處:landing page 說「目前不會被收費」,pricing page 列出費率且沒有任何 beta 標示;pricing changelog 公告了 Workers Free 方案的 1 GB 額度,pricing page 的表格裡沒有這一欄;官方唯一講 binding 設定的頁面用的還是舊鍵名。

(四)官方從未說明底層引擎。 網路上(包括本系列早期的大綱草稿)流傳 Pipelines 建在 Arroyo 之上。翻遍官方文件找不到任何一處提到 Arroyo。本章不做這個宣稱。

所以本章的定位是:幫你判斷現在該不該用,以及如果要用,哪些地方文件會騙你


HTTP POST ─┐
├─► Stream ──► Pipeline (SQL) ──► Sink ──► R2 / R2 Data Catalog
Worker send() ─┘
物件是什麼官方定義
Stream入口「持久化、有緩衝的佇列,透過 HTTP 端點或 Worker binding 接收事件」
Pipeline轉換「用 SQL 轉換連接 stream 與 sink,在寫入儲存前修改事件」
Sink出口Iceberg 表(R2 Data Catalog)或 R2 上的 Parquet / JSON 檔

一條完整的鏈需要三個物件都建立。wrangler pipelines setup 是互動式的一次建全套,實測 CLI 表面:

wrangler pipelines setup Interactive setup for a complete pipeline
wrangler pipelines create <pipeline> Create a new pipeline
wrangler pipelines list / get / delete
wrangler pipelines update <pipeline> (legacy pipelines only)
wrangler pipelines streams create / list / get / delete
wrangler pipelines sinks create / list / get / delete

注意 streamssinks 都沒有 update 子指令。加上下一節會提到的「pipeline SQL 建立後不可修改」,這個平台目前的心智模型是:所有設定都是 immutable,要改就刪掉重建

Terminal window
npx wrangler pipelines streams create ch23-clicks --schema-file schema.json

實測 --help 的完整選項:

--schema-file Path to JSON file containing stream schema
--http-enabled Enable HTTP endpoint [default: true]
--http-auth Require authentication [default: true]
--cors-origin CORS origin [array]

兩個 default 值得記下來,因為官方的 manage-streams 頁面只用 dashboard 截圖說明,從來沒寫出這兩個預設值:HTTP 端點預設開啟,而且預設要求驗證

schema 是可選的。給了就會驗證與強制執行,不給就接受任何合法 JSON。schema 檔長這樣:

{
"fields": [
{ "name": "tenant_id", "type": "string", "required": true },
{ "name": "slug", "type": "string", "required": true },
{ "name": "country", "type": "string", "required": false },
{ "name": "latency_ms","type": "int32", "required": false },
{ "name": "ts", "type": "timestamp", "required": true }
]
}

支援型別:stringint32int64float32float64booltimestamp(RFC 3339 或 Unix 數字)、jsonbinary(base64)、list(需 items)、struct(巢狀 fields)。

這裡有一個必須加粗的行為。 官方 manage-streams 頁面寫著:「不符合已定義 schema 的事件在 ingestion 時會被接受,但在處理階段會被丟棄。」

也就是說:你 POST 進去會拿到 200,資料卻永遠不會出現在 sink 裡。沒有 dead letter queue,沒有錯誤回應。唯一能看見的地方是 metrics —— GraphQL dataset pipelinesUserErrorsAdaptiveGroups,錯誤類型有 missing_fieldtype_mismatchparse_failurenull_value

對照第 19 章:Queues 有 DLQ,訊息失敗會進死信佇列讓你重放。Pipelines 目前沒有等價機制。所以「上線第一週每天檢查 error metrics」不是建議,是必要作業。

POST https://{stream-id}.ingest.cloudflare.com
Authorization: Bearer <API_TOKEN>
Content-Type: application/json
[{"tenant_id":"t1","slug":"abc","ts":"2026-08-01T05:00:00Z"}]

body 是 JSON 陣列,不是 NDJSON。NDJSON 只出現在 R2 sink 的輸出格式裡,兩者不要搞混。

Terminal window
npx wrangler pipelines create ch23-pipeline --sql-file transform.sql
INSERT INTO ch23_sink
SELECT
tenant_id,
slug,
upper(country) AS country,
latency_ms,
ts
FROM ch23_clicks
WHERE tenant_id IS NOT NULL;

一個檔案裡可以放多個 INSERT INTO ... SELECT ... FROM ...,用分號隔開 —— 這就是 fan-out(一份 stream 寫進多個 sink)的做法。

官方明確寫出、而且非常重要的一句話:

「Pipeline SQL 建立後無法修改。要改變 SQL 轉換,你必須刪除並重建這條 pipeline。」

實務上這代表 SQL 轉換要當成 schema migration 等級的變更來對待,走 CI/CD 而不是手改(第 40 章會處理這件事)。

關於「只支援 stateless 轉換」這個說法,本章必須誠實。 官方 SELECT 文法參考給的完整文法只有:

[WITH with_query [, ...]] SELECT select_expr [, ...] FROM from_item [WHERE condition]

加上 UNNEST(「只能出現在 SELECT 子句」、「每個 SELECT 只能 unnest 一個陣列」)。GROUP BYHAVINGORDER BYLIMITJOINDISTINCTUNION、window function 全都不在文法裡。pricing 頁面另外有一句旁證:定價「涵蓋 stateless 轉換(例如 filter、reshape、unnest、cast、compute)」,而聚合這類 stateful 操作「未來可能另外計價」。

但是 —— 官方從來沒有明說這些子句「不支援」。 所以本章的說法是:

官方文件所記載的文法只允許 WITH / SELECT / FROM / WHERE / UNNEST。要做聚合,請在下游(R2 SQL 或你自己的引擎)做,不要指望在 pipeline 裡做。

這是刻意的措辭差異。「文法只記載了這些」和「這些一定會被 parser 拒絕」是兩件事,我沒有帳號可以實測後者。

兩種類型,實測 wrangler pipelines sinks create --help 的完整選項與預設值:

--type [required] [choices: "r2", "r2-data-catalog"]
--bucket [required]
--format [choices: "json", "parquet"] [default: "parquet"]
--compression [choices: "uncompressed","snappy","gzip","zstd","lz4"] [default: "zstd"]
--target-row-group-size Target row group size for parquet format
--path The base prefix in your bucket where data will be written
--partitioning Time partition pattern (r2 sinks only)
--roll-size Roll file size in MB
--roll-interval Roll file interval in seconds [default: 300]
--access-key-id / --secret-access-key (留空則自動建立 R2 credentials)
--namespace / --table / --catalog-token (r2-data-catalog 必填)

幾個文件與 CLI 對得起來、但值得單獨拉出來的點:

  • --format預設是 parquet,不是 json。json 產出的是 newline-delimited JSON。
  • --partitioning 的說明字串直接寫著 (r2 sinks only) —— R2 Data Catalog sink 沒有這個選項(Iceberg 自己管分割)。預設 pattern 是 year=%Y/month=%m/day=%d,用 strftime 指示字元。
  • --roll-interval 預設 300 秒。R2 sink 最小 10 秒,R2 Data Catalog sink 最小 60 秒,官方理由是「避免壓實(compaction)問題」。
  • R2 Data Catalog sink 無法對既有的 Iceberg 表建立。 官方原文:「sink 會建立指定的 namespace 與 table(若不存在)。Sinks cannot be created for existing Iceberg tables.」如果你已經有一張 Iceberg 表想灌進去,目前做不到。

關於 exactly-once:官方只在 sinks 總覽頁說了一句「Sinks 提供 exactly-once 投遞保證,確保事件不會重複也不會遺失」。沒有任何一頁定義這個保證的範圍 —— 是端到端(含 HTTP 重送、含 Worker 重試),還是只保證 sink 寫入端?本章不替它背書。你的 Worker 若在 send() 失敗後自行重試,重複的責任在你這邊,不在 sink。


23.3 Worker binding:本章實測最有價值的一段

Section titled “23.3 Worker binding:本章實測最有價值的一段”

鍵名改過,而唯一講 binding 的文件頁還是舊的

Section titled “鍵名改過,而唯一講 binding 的文件頁還是舊的”
{
"pipelines": [
{ "binding": "EVENTS", "stream": "ch23-clicks" }
]
}

注意:最外層的陣列仍然叫 pipelines,改的是裡面那個鍵:pipelinestream

node_modules/wrangler/config-schema.json,這件事寫得清清楚楚:

{
"binding": { "type": "string" },
"stream": { "type": "string", "description": "Id of the Stream to bind" },
"pipeline": {
"type": "string",
"description": "Id of the Stream to bind",
"deprecated": "Use `stream` instead."
},
"remote": {
"type": "boolean",
"description": "Whether the pipeline should be remote or not in local development"
}
}

實測用舊鍵,wrangler 每一次指令都會警告:

▲ WARNING Processing wrangler.jsonc configuration:
- The "pipeline" field in "pipelines[1]" bindings is deprecated. Use "stream" instead.

問題在於:官方唯一一頁講「如何從 Worker 寫入 stream」的文件(/pipelines/streams/writing-to-streams/),到今天用的還是 "pipeline" 舊鍵,而且沒有任何棄用註記。 改名這件事只出現在 changelog 裡。所以一個照著文件做的讀者,會得到一個每次 build 都跳警告的專案,而且不知道為什麼。

(旁證:官方的 Bluesky firehose 範例頁用的是新鍵 "stream"。同一份文件站的兩頁互相矛盾。)

schema 的 required 只有 ["binding"]。所以照著 JSON schema 的編輯器自動完成,你會以為 { "binding": "X" } 是合法的。實測:

✘ ERROR Processing wrangler.jsonc configuration:
- "pipelines[2]" bindings must have a string "stream" field but got {"binding":"NO_TARGET"}.

JSON schema(給編輯器看的)比 wrangler 的執行期 validator 寬鬆。 這是本系列第三種「三個真相來源互相不符」的情況:第 15 章是型別比 runtime 窄、第 21 章是文件比型別窄,這裡是 schema 比 validator 寬。遇到設定問題時,wrangler deploy --dry-run 的實際輸出為準,不要以編輯器的紅線為準。

remote 支援:文件沒說,schema 說了

Section titled “remote 支援:文件沒說,schema 說了”

官方的 bindings-per-env 支援矩陣 列出了 AI、Assets、Analytics Engine、Browser、D1、DO、Containers、Email、Hyperdrive、Images、KV、Queues、R2、Rate Limiting、Service Bindings、Vectorize、Workflows —— Pipelines 整個不在表上。沒有任何一頁提到 wrangler dev 對 Pipelines 的行為。

但 schema 裡有 remote,而且描述就是「Whether the pipeline should be remote or not in local development」。實測加上 "remote": truewrangler deploy --dry-run 完全不報錯(對照第 22 章的 Analytics Engine 加 remote 會出警告)。

結論:Pipelines 支援 remote binding,只是沒有寫進文件。

本地 binding 的真身:它是一個 Fetcher

Section titled “本地 binding 的真身:它是一個 Fetcher”

這是本章最有趣的發現。把 binding 的原型鏈整條印出來:

{
"typeofBinding": "object",
"hasSend": true,
"typeofSend": "function",
"sendSource": "[object JsRpcProperty]",
"protoChain": [
{ "ctor": "Fetcher", "keys": [] },
{ "ctor": "Fetcher", "keys": ["fetch", "connect", "constructor"] },
{ "ctor": "Object", "keys": ["constructor", "hasOwnProperty", ...] }
]
}

三件事:

  1. 原型是 Fetcher —— 就是第 18 章 service binding 的那個類別。
  2. 原型上根本沒有 send 原型只有 fetchconnect
  3. String(binding.send)"[object JsRpcProperty]"

也就是說:Pipelines binding 是建在 Workers RPC 之上的(第 18 章)。send 不是一個真的方法,是一個 RPC proxy property。這解釋了它的所有行為,也帶來一個具體的陷阱 —— 任何屬性存取都會產生一個看起來像函式的 proxy:

{
"typeofTypo": "function",
"typoSource": "[object JsRpcProperty]",
"callTypo": { "threwName": "TypeError",
"threw": "TypeError: The RPC receiver does not implement the method \"sendd\"." },
"callFetch": { "threwName": "Error",
"threw": "Error: Handler does not export a fetch() function." }
}

env.EVENTS.senddtypeof"function""sendd" in env.EVENTS 也是 true。只有真的呼叫下去才會炸。所以 typeof x.send === "function" 這種 feature detection 對這個 binding 完全無效 —— 它對任何名字都回 true。

順帶一提,binding.fetch()Handler does not export a fetch() function,證實遠端接收者是純 RPC entrypoint。

型別是:

export interface Pipeline<T extends PipelineRecord = PipelineRecord> {
send(records: T[]): Promise<void>;
}
export type PipelineRecord = Record<string, unknown>;

實測七個探針,全部 resolve、全部回 undefined

探針結果
send([{...}]) 正常ok
send(50 筆)ok
send([]) 空陣列ok
send({a:1}) 非陣列ok
send() 不給參數ok
send([{a:{b:[1,2,{c:"d"}]}}]) 巢狀ok
send([{fn: () => 1}]) 不可序列化ok

.wrangler/state/v3/ 底下只有 cacheobservabilityworkflows —— 沒有 pipelines 目錄,什麼都沒有存下來

這和第 22 章的 Analytics Engine 是同一個病:本地 binding 是不驗證、不儲存的 no-op。差別在於 Analytics Engine 至少有一個自己的類別 LocalAnalyticsEngineDataset,Pipelines 連那個都沒有,就是一個空的 RPC receiver。

實務對策:既然 remote: true 可用(即使文件沒寫),開發 Pipelines 就該用它。 這是本章唯一能真正驗證資料有沒有進去的方法。

實測 wrangler types 在未登入時的警告:

▲ WARNING Not authenticated - using generic types for pipeline bindings.
Run `wrangler login` to enable typed pipeline bindings.

未登入時退回泛型:

EVENTS: import("cloudflare:pipelines").Pipeline<import("cloudflare:pipelines").PipelineRecord>;

登入而且 stream 有 schema 時,wrangler 會去讀 schema 產生具名型別(PascalCase + Record 後綴),例如 Cloudflare.Ch23ClicksRecord。這代表 stream schema 是型別安全的來源,值得為此宣告 schema,而不只是為了驗證。

worker-configuration.d.tscloudflare:pipelines 模組裡還有這個:

export abstract class PipelineTransformationEntrypoint<Env, I, O> {
public run(records: I[], metadata: PipelineBatchMetadata): Promise<O[]>;
}
export type PipelineBatchMetadata = { pipelineId: string; pipelineName: string };

這是舊模型的 Worker-based 轉換:一個 Worker 類別接收一批 records、回傳轉換後的 records。新架構的轉換是 SQL,這個類別在現行文件裡完全找不到。它還留在 workerd 的型別裡,因為 legacy pipeline 還在運作。

不要用它。 它屬於 2025-09-25 之前的模型,會隨著 legacy pipeline 一起消失。


R2 Data Catalog 是「直接內建在 R2 bucket 裡的受管 Apache Iceberg 資料目錄」,對外暴露標準的 Iceberg REST catalog 介面。

Terminal window
npx wrangler r2 bucket catalog enable linkforge-lake

回傳兩個東西:Warehouse 名稱與 Catalog URI。之後任何支援 Iceberg REST catalog 的引擎都能接:

from pyiceberg.catalog.rest import RestCatalog
catalog = RestCatalog(
name="linkforge",
warehouse=WAREHOUSE,
uri=CATALOG_URI,
token=R2_TOKEN, # R2 API token, Admin Read & Write
)

官方有 Trino、DuckDB、PyIceberg、Snowflake、Spark(Python / Scala)、StarRocks 的分別說明頁。這是整個 Cloudflare Data Platform 最有戰略價值的一點:資料以開放格式(Iceberg on Parquet)存在你自己的 R2 bucket 裡,不是專有格式。要離開這個平台,把 bucket 同步走就行;第 12 章講過 R2 沒有 egress 費用,所以連搬走都不用付出口費。這在 vendor lock-in 的權衡上是很不一樣的立場。

實測 CLI:

wrangler r2 bucket catalog enable / disable / get <bucket>
wrangler r2 bucket catalog compaction 自動壓實維護作業
wrangler r2 bucket catalog snapshot-expiration 自動快照過期

串流寫入必然產生大量小檔案,小檔案會殺死查詢效能。R2 Data Catalog 提供受管壓實,但預設不開

Terminal window
npx wrangler r2 bucket catalog compaction enable linkforge-lake \
--target-size 128 --token $R2_CATALOG_TOKEN

--target-size 範圍 64–512 MB。官方建議值:延遲敏感 64–128、串流攝入 128–256、OLAP 256–512。

Beta 期間的限制值得記下來:「每張表每小時最多壓實 2 GB 的檔案。」 如果你的攝入速率高於這個,小檔案會持續累積。以 --roll-interval 300(預設 5 分鐘)計算,一天會產生 288 個檔案;資料量大時很快就會撞到這個上限。

另外兩點:只支援 Parquet;孤兒檔案不會被清理。快照過期是免費的。


⚠️ 首先更正一個常見錯誤:文件不在 /r2/sql/(那會 404),而在 /r2-sql/

R2 SQL 是「Cloudflare 的 serverless 分散式分析查詢引擎,用來查詢儲存在 R2 Data Catalog 的 Apache Iceberg 表」。官方沒有指名底層用了哪個第三方引擎。

Terminal window
export WRANGLER_R2_SQL_AUTH_TOKEN="<token>"
npx wrangler r2 sql query <WAREHOUSE> "SELECT * FROM linkforge.clicks LIMIT 10"

實測 CLI 簽章 —— 兩個必填位置參數,沒有任何指令專屬的旗標

POSITIONALS
warehouse R2 Data Catalog warehouse name [required]
query The SQL query to execute [required]

HTTP API:

POST https://api.sql.cloudflarestorage.com/api/v1/accounts/{ACCOUNT_ID}/r2-sql/query/{BUCKET_NAME}
Authorization: Bearer ${WRANGLER_R2_SQL_AUTH_TOKEN}
{ "query": "SELECT * FROM namespace.table_name limit 10;" }

注意這個 host 不是 api.cloudflare.com —— 是獨立的 api.sql.cloudflarestorage.com

能力已經比 2025 年的說法強很多

Section titled “能力已經比 2025 年的說法強很多”

如果你讀過 2025 年的 R2 SQL 教學說「不支援 JOIN、不支援聚合」,那個資訊已經過時。 現行 SQL 參考記載的是:

  • SELECTFROMWHEREGROUP BYHAVINGORDER BY(ASC/DESC、多欄位)、LIMIT
  • JOIN:INNER、LEFT、RIGHT、FULL OUTER、CROSS
  • CTE、FROM / IN / EXISTS 子查詢、EXPLAIN
  • 有獨立的聚合函式參考頁

明確不支援的(這一份清單很值得抄下來):

不支援備註
OFFSETLIMIT 只吃整數,預設 500
LATERAL derived tableFROM 子查詢不能引用其他 FROM 欄位
nullable 欄位上的 NOT IN 子查詢
子查詢裡的 SELECT DISTINCT
巢狀(括號)join
具名 WINDOW 宣告
INSERT / UPDATE / DELETE 與所有 DDL「R2 SQL 是查詢引擎,不是資料庫。No writes.」
Parquet 以外的格式

還有一個很特別的設計:部分函式會在執行前做預算檢查,掃描量過大就直接回 400 —— MEDIANPERCENTILE_CONTARRAY_AGGSTRING_AGG、window function 都在這一類。這是我在其他查詢引擎上沒看過的做法:不是跑到一半超時,是根本不讓你跑。

另外一個小細節:now() / current_time() 「量化到 10ms 邊界並強制 UTC」。

第 22 章的 Analytics Engine 至少有一張七行的 limits 表。R2 SQL 完全沒有 limits 頁面。 limitations-best-practices 是純散文,沒有任何數字:沒有查詢逾時秒數、沒有掃描量上限、沒有結果大小上限、沒有並行數。

唯二的量化事實來自 pricing 頁:每次查詢最低計費 10 MB 掃描量,以及上面那個 400 預算門檻。

所以:任何宣稱 R2 SQL 有某個查詢逾時或掃描上限的教學(包括別處看到的),都不是從官方文件來的。 本章不臆測。

query-data 頁列了三項要求,而且同一頁內部就自相矛盾 —— 文字說「R2 Data Catalog(read-only)」,對應的權限群組卻寫 Workers R2 Data Catalog Write

get-started 頁則簡單得多:建一個 Admin Read & Write 的 R2 API token,「這也包含了 R2 SQL Read 權限」。

實務建議:照 get-started 走,用 Admin Read & Write。


Pipelines limits(官方整張表就這五行)

Section titled “Pipelines limits(官方整張表就這五行)”
項目上限
每帳號 streams 數20
每次攝入請求的 payload5 MB
每個 stream 的攝入速率5 MB/s
每帳號 sinks 數20
每帳號 pipelines 數20

可透過 Limit Increase Request Form 申請提高。

文件沒有記載的:單筆 record 大小上限、單次 send() 的筆數上限、schema 欄位數上限、一條 pipeline 的 SQL 敘述數上限。唯一相關的數字是 5 MB 的攝入 payload 上限,而文件並沒有說這條是否適用於 send()

「每帳號 20 個 stream」這條對多租戶設計有直接影響:你不能一個租戶開一個 stream。正確做法是所有租戶共用一條 stream,tenant_id 當成欄位,在 sink 的分割或查詢時再切開。

Pipelines

  • Landing page(/pipelines/):「Pipelines 處於 open beta,任何有 Workers Paid 方案的開發者都能開始使用。」以及「目前,除了標準的 R2 儲存與操作費用之外,你不會因為使用 Pipelines 而被收費。」
  • Pricing changelog:「Billing is not yet enabled. 我們會在開始收費前至少提前 30 天通知。」並公告 Workers Free 每個維度每月 1 GB、Paid 每月 50 GB。
  • Pricing 頁(/pipelines/platform/pricing/):直接列出費率 —— Streams 攝入免費、SQL 轉換 $0.04/GB、Sink 寫出 R2-JSON $0.03/GB、Parquet/Iceberg $0.06/GB。沒有 beta 標示、沒有「尚未開始計費」的說明、而且表格裡只有 Workers Paid 一欄,changelog 公告的 Free 方案 1 GB 額度不見了。

R2 SQL 是完全一樣的模式:landing page 說 open beta 且「不會被收費」,pricing 頁列出 $0.0025/GB 掃描($2.50/TB)、每月含 10 GB、每次查詢最低計費 10 MB、失敗的查詢不計費、EXPLAIN/SHOW/DESCRIBE 免費 —— 沒有 beta 或「尚未計費」的字樣。

R2 Data Catalog 也一樣:public beta 且「不會被收費」,pricing 頁列出每百萬 catalog 操作 $9.00、壓實處理 $0.005/GB、每百萬物件 $2.00。

三個產品、三組矛盾、同一個模式。

本章的立場:以 landing page 的「open beta,尚未計費」為現況,以 pricing 頁的費率為未來的成本估算依據。查證日期 2026-08-01,讀者請自行重新確認。

架構決策上要注意的是:這三個產品的費率不是零,而且 Pipelines 的 SQL 轉換與 sink 寫出是分開計價的。一份資料經過轉換再寫成 Parquet,是 $0.04 + $0.06 = $0.10/GB。以 LinkForge 每天 1 億次點擊、每筆 200 bytes 估算,每天 20 GB、每月 600 GB,扣掉含量 50 GB 之後大約 $55/月。這個數量級要事先算清楚。


綜合前面所有內容,我的判斷框架如下。這不是官方立場 —— 官方沒有任何一頁比較這幾個產品。

該用的情境:

  • 你需要稽核軌跡:逐筆、不抽樣、可長期保存。Analytics Engine(第 22 章)做不到這件事。
  • 你需要開放格式的資料湖:Iceberg on Parquet 存在自己的 R2 bucket,可以被 Spark / DuckDB / Trino 讀,將來要搬走沒有 egress 費用。
  • 你的分析團隊已經在用 Iceberg 生態系。
  • 你需要超過 3 個月的歷史資料。

先別用的情境:

  • 你只需要儀表板與趨勢 —— Analytics Engine 更簡單、更便宜(目前免費)、查詢延遲更低。
  • 你需要即時查詢 —— sink 的 roll interval 預設 300 秒、R2 Data Catalog sink 最小 60 秒,資料落地本來就有分鐘級延遲。
  • 你需要在攝入階段做聚合 —— 文法不允許。
  • 你無法承受 open beta 的變動。 這個產品在 2025-09 整個重寫過一次,binding 鍵名在 2026-05 改過名,計費隨時可能開啟。

三個產品的分工,放進 LinkForge 的架構:

需求用什麼
即時點擊計數Durable Object 計數器(第 14 章)
儀表板趨勢、前 N 名Analytics Engine(第 22 章)
逐筆稽核、長期歸檔Pipelines → R2 Data Catalog(本章)
臨時的深度分析查詢R2 SQL(本章)
精確計費用量DO 計數器 + 每日對帳寫入 D1

同一筆點擊事件同時寫進 Analytics Engine 和 Pipelines 是完全合理的 —— 前者負責快而粗,後者負責慢而全。兩邊各自的成本模型都是線性的,加起來也還是可預測的。


#結論影響
1config-schema.jsonpipeline 標為 deprecated: "Use \stream` instead.”`鍵名改名證據確鑿
2用舊鍵每次指令都出警告,訊息文字已記錄於本章官方唯一的 binding 文件頁仍用舊鍵,照做會一直看到警告
3schema 只要求 binding,validator 要求 stream 是字串schema 比 validator 寬鬆,以 --dry-run 為準
4schema 有 remote 欄位且 --dry-run 接受Pipelines 支援 remote binding,文件完全沒說(且不在支援矩陣裡)
5本地 binding 的原型鏈是 Fetchersend[object JsRpcProperty]Pipelines 建在 Workers RPC(第 18 章)之上
6任何屬性名的 typeof 都是 "function"env.X.sendd(...) 才丟 The RPC receiver does not implement the method "sendd"feature detection 對這個 binding 無效
7binding.fetch()Handler does not export a fetch() function遠端是純 RPC entrypoint
8send() 對不給參數、非陣列、空陣列、不可序列化值全部靜默成功本地無驗證
9.wrangler/state/v3/ 下沒有 pipelines 目錄本地不持久化 —— 開發必須用 remote: true
10wrangler types 未登入時警告並退回 Pipeline<PipelineRecord>具名型別需要登入 + stream schema
11runtime 型別仍保留 PipelineTransformationEntrypoint(舊的 Worker 轉換模型)現行文件已無此物,不要使用
12CLI 每個子指令都標 [open beta]pipelines update(legacy pipelines only)舊模型仍在,但只維護不演進
13streams / sinks 都沒有 update 子指令;pipeline SQL 建立後不可改全平台 immutable,改動 = 刪除重建
14--http-enabled--http-auth 預設都是 true文件從未寫出這兩個預設值
15--format 預設 parquet;--partitioning 標註 (r2 sinks only)Data Catalog sink 沒有分割選項
16不符合 schema 的事件「攝入時被接受、處理時被丟棄」,且沒有 DLQ只能靠 metrics 發現;對照 Queues 有 DLQ
17R2 Data Catalog sink 無法對既有 Iceberg 表建立既有資料湖無法直接接上
18R2 SQL 文件在 /r2-sql/ 而非 /r2/sql/(後者 404)
19R2 SQL 現已支援 JOIN / GROUP BY / HAVING / CTE2025 年「不支援聚合」的說法已過時
20R2 SQL 沒有 limits 頁;無任何逾時或掃描上限數字引用這類數字的教學都不是來自官方
21部分函式(MEDIAN、window 等)掃描量過大時執行前回 400預算門檻,不是逾時
22Compaction 是 opt-in;beta 期間每張表每小時上限 2 GB高攝入速率下小檔案會累積
23三個產品的 landing page 說「不會被收費」,pricing 頁列出費率且無 beta 標示需標註查證日期並自行確認
24每帳號 20 個 stream / sink / pipeline不能一租戶一 stream;共用 stream + tenant_id 欄位
25官方文件從未提及 Arroyo 或任何底層引擎網路上的說法無官方來源

  1. wrangler pipelines setup 建一條完整的鏈,然後在 Worker 裡設 "remote": true,用 wrangler dev 實際把資料送進去 —— 對照 23.3 的本地 no-op 行為。
  2. 故意送一筆缺少 required 欄位的事件,確認 HTTP 回 200,然後從 GraphQL pipelinesUserErrorsAdaptiveGroups 找出 missing_field 計數。這是驗證「靜默丟棄」的唯一方法。
  3. 對同一份 LinkForge 點擊資料,分別用 Analytics Engine 的 sum(_sample_interval)(第 22 章)與 R2 SQL 的 count(*) 算一次日點擊數,比較抽樣造成的誤差。
  4. 開啟 compaction,觀察 --target-size 128 前後 R2 SQL 同一個查詢的掃描量(用 EXPLAIN,免費)。
  5. 用 PyIceberg 從本機連上 R2 Data Catalog 讀同一張表,驗證「開放格式、可攜」這個賣點。

第 23 章結束了 Part 4(資料與分析)。下一章起進入 Part 5:前端與全端整合 —— 第 24 章從 React Router v7 的 framework mode 開始,把 LinkForge 的後台介面接上來。