Cloudflare Workers 移动端后端:日均百万请求,月费 $5

基于 Cloudflare Workers 的移动端后端,月费仅 $5、日均处理百万请求。实战架构:D1、R2、KV、Durable Objects,冷启动 p99 50ms,无需 Node 服务器。

Huifer
Huifer
May 8, 202612 min read
其他语言:Deutsch · English

本文由 Huifer 撰写,独立开发者、TanStack Ship 维护者。2025 年 10 月至 2026 年 4 月,我在 Cloudflare Workers 上运行了一个移动端后端(iOS + Android)。峰值负载:日均 110 万请求。以下架构、月费 $5/mo 的成本数字、以及 50ms p99 冷启动数据均来自该生产构建,而非基准实验室。30 秒 CPU 限制和 iOS NSURLSession 的一些特殊行为也来自该构建。

已验证来源: Cloudflare Workers 文档 · Cloudflare D1 文档 · Cloudflare R2 文档 · Cloudflare KV 文档 · Cloudflare Durable Objects · TanStack Start Server Functions · Stripe Mobile Backend Patterns · TanStack Ship GitHub

最后更新: 2026-05-08 · 更新日志

TL;DR: 基于 Cloudflare Workers 的移动端后端是真实可用的生产级方案,成本极低。参考架构四个绑定——D1 存储关系数据、R2 存储媒体文件、KV 做缓存和配置、Durable Objects 提供实时能力——每个路由组对应一个 Worker。冷启动 p99 50ms。Workers 付费计划月费 $5(每月 3000 万请求)。R2 月费 $0.08(10GB)。以下是架构设计、基准测试方法论、生产级代码、成本计算,以及四个你应该选择其他技术栈的场景。


为什么移动端后端用 CF Workers 不一样

Cloudflare Workers 常被介绍为"边缘计算的无服务器函数"。这种表述低估了它的真实本质。Worker 是一个 V8 隔离环境(参见 Workers 运行时文档),在数毫秒内运行你的 JavaScript 或 TypeScript,部署在 330+ 个城市,并共享四个存储原语的绑定:D1(边缘计算版 SQLite)、R2(S3 兼容对象存储,零出口流量费用)、KV(最终一致键值存储)和 Durable Objects(单线程协调状态)。正是这种组合让 Workers 上的移动端后端真正不同于 AWS Lambda 或 Google Cloud Functions——存储原语是一等公民,而非事后补救。

对于移动端后端,这个区别在三个方面至关重要:

  1. 移动端客户端自动接入最近的数据中心。 无需区域配置、无需 CloudFront 分发、无需延迟路由规则。柏林的访客命中法兰克福 Worker;圣保罗的访客命中圣保罗 Worker。p50 TTFB 从 180ms(单区域 Node)降至 35ms。
  2. 存储绑定与计算共置。 D1 的读写与 API 处理器在同一个 V8 隔离环境中执行。没有到数据库集群的网络跳数。即使在冷路径上,p99 读延迟也低于 100ms。
  3. 成本由请求数决定,而非实例时长。 按小时计费的 Node 服务器无论服务 100 个还是 10 万个请求,成本相同。Workers 按请求计费,免费额度充足,超出后按每百万请求可预测定价。参见 Workers 定价 页面了解当前费率。

我用这个技术栈运行移动端后端七个月,峰值日均 110 万请求,单人独立开发,从未开过 pager。以下是该架构、数字、以及产生这些数字的代码。

"边缘"对移动端用户实际意味着什么

移动端客户端不知道也不关心它连接的是一个边缘运行时。从客户端角度看,它发送一个 HTTPS 请求并获得 JSON 响应。边缘路由在 Cloudflare 网络中透明完成。收益体现在尾延迟改善——生产移动端 API 在日均 110 万请求下全球 p99 TTFB 为 92ms,而此前托管在单个 us-east-1 EC2 实例上的相同 API 为 410ms。4 倍的尾延迟改善就是用户在网络不佳时感受到的"App 响应快"。

如果有需要,相同的 Worker 还可以做地理围栏路由:Workers 付费用户可以配置区域过滤,使来自欧盟 IP 的请求仅在欧盟数据中心运行。这对有数据驻留合规要求的 GDPR 应用至关重要。

你真正会用到的四个绑定

Workers 上的移动端后端不需要每一个 Cloudflare 产品,只需要四个,用对场景。

D1 用于关系数据(认证、计费、订阅)

D1 是 Cloudflare 的分布式 SQLite 数据库——参见 D1 文档 了解完整接口。对于移动端后端,D1 存储用户、会话、订阅、应用内购买、推送令牌以及其他关系数据。与 Worker 共置时读写延迟低于 1ms,写入持久化且有复制。

典型移动 SaaS 的 Schema 只需 12 张表。我的包含:users、sessions、subscriptions、push_tokens、devices、in_app_purchases、support_tickets、feature_flags、audit_log、content_versions、ab_assignments 和 webhook_events。总大小:18MB。D1 每个数据库 10GB 的限制在需要分片之前都不是问题。

KV 用于缓存 + 配置(功能开关、用户偏好)

Cloudflare KV 是最终一致的键值存储。最终一致性(全球 60 秒传播窗口)对功能开关、用户偏好、A/B 测试分组和配置没问题——任何允许读取略微过期值的场景都适用。KV 读取在免费计划下每天前 1000 万次免费,这足以覆盖大多数移动端后端很长时间。

最难内化的模式是:不要在需要一致性的场景使用 KV。订阅状态、支付状态和认证凭据应放在 D1。功能开关、主题偏好和引导步骤索引应放在 KV。

R2 用于媒体文件(头像、照片、导出)

R2 是 S3 兼容对象存储,零出口流量费用。对于移动端后端,R2 存储用户上传的媒体:头像、照片附件、导出的报告、生成的 PDF、语音备忘。零出口流量计费模式很重要,因为移动端客户端带宽有限;你不会想为每次向活跃用户发送一个 2MB 头像按字节付费。

R2 定价简洁:$0.015/GB/月,无出口费用。10GB 用户照片月费 $0.15。同等负载在 S3 上存储费 $0.23/月,加上约 $90/月的出口费用(1TB 传输量)。

Durable Objects 用于实时能力(在线状态、实时同步)

Durable Objects 是单线程 JavaScript Worker,在内存中保存状态并协调所有客户端的访问。对于移动端后端,Durable Objects 为实时功能提供支持:在线状态指示器(谁在线)、协作编辑、实时通知、跨设备同步状态。

参考模式:每个用户一个 Durable Object,在内存中保存状态,移动端客户端通过 WebSocket 连接获取实时更新。DO 按请求和时长计费;每 100 万 DO 请求预算 $0.15-$0.50/月。

最先遇到的限制:Durable Objects 是单线程的。如果你在热路径上构建了一个做同步 CPU 工作的 DO(图片处理、大型 JSON 序列化、ML 推理),该 DO 上的每个并发 WebSocket 客户端都要等待。解决方案是在普通 Worker 中做重计算工作,让 DO 仅做状态编排。这与单线程 Node 事件循环的思维模型相同——仅非阻塞 I/O。

50ms p99 冷启动数字(以及我们如何测量它)

Workers 的核心数字是冷启动延迟。Cloudflare 营销页面声称"接近零冷启动"——这说法正确但模糊。以下是我们实际测量的结果。

基准测试方法论

我在生产 API 上添加了计时头(X-Worker-Start、X-Worker-Duration),并连续 30 天记录每次冷启动。冷启动定义为 15 分钟空闲窗口后对 Worker 的第一次请求(Worker 隔离回收的默认间隔)。中间的所有请求均为暖请求。

冷启动分布(p50/p90/p99)

30 天内,14 个 API 端点上共记录了 18,400 次冷启动:

  • p50: 18ms
  • p90: 38ms
  • p99: 50ms
  • p99.9: 84ms
  • 最大值: 220ms(一个特定端点,导入图达 1.2MB)

对比参考:同一时间段内 AWS Lambda 上 Node Express 服务器 p99 冷启动为 1,400ms。V8 隔离环境比容器化无服务器冷启动快约 30 倍,因为没有容器编排、没有 Docker 拉取、没有操作系统启动。

220ms 的异常值追溯到该端点有一个很重的导入图——Stripe SDK + 4 个 Drizzle Schema + 3 个同级 Server Functions 全写在一个文件里。修复方案(通过动态导入进行路由级代码分割)是另一个值得单独复盘的主题。架构良好的 Worker 核心数字是 p99 50ms。

打包体积为何影响冷启动

Worker 的冷启动成本与编译后 JavaScript 打包体积大致呈线性关系。100KB 时冷启动几乎不可测量;500KB 时约 30-40ms;1MB 时约 80-120ms;5MB 则逼近 Workers 打包体积限制,冷启动不再可靠。

对移动端后端开发者的启示:保持每个 Worker 的导入图精简。如果你在某个 Server Function 中才需要 Stripe,就在那个函数内部动态导入 Stripe,而非在共享模块顶部导入。TanStack Ship 的 Server Functions 模板默认强制这一做法——每个路由组通过路由的懒加载模块获得独立打包。

真实架构:TanStack Ship 参考技术栈

TanStack Ship 交付的移动端后端使用 TanStack Start Server Functions 作为 API 层面。每个路由组是一个 Server Function,仅导入其需要的绑定。

typescript
// app/server/auth.ts — 移动端认证端点
import { createServerFileRoute } from '@tanstack/start/server';
import { db } from '~/lib/db';
import { users, sessions, devices } from '~/lib/schema';
import { eq } from 'drizzle-orm';
import { signJWT } from '~/lib/jwt';

export const ServerRoute = createServerFileRoute('/api/mobile/auth/login').methods({
  POST: async ({ request }) => {
    const { email, password, deviceId, pushToken } = await request.json();

    // D1 查询 — 边缘计算下亚毫秒级
    const user = await db.select().from(users).where(eq(users.email, email)).get();
    if (!user) return Response.json({ error: 'invalid_credentials' }, { status: 401 });

    const valid = await verifyPassword(password, user.passwordHash);
    if (!valid) return Response.json({ error: 'invalid_credentials' }, { status: 401 });

    // 为移动端客户端签发 JWT
    const jwt = await signJWT({ userId: user.id, deviceId });

    // 在单个 D1 事务中持久化会话和设备
    await db.batch([
      db.insert(sessions).values({
        id: jwt.id,
        userId: user.id,
        deviceId,
        createdAt: Date.now(),
        expiresAt: Date.now() + 30 * 24 * 60 * 60 * 1000,
      }),
      db.insert(devices).values({
        id: deviceId,
        userId: user.id,
        pushToken,
        lastSeenAt: Date.now(),
      }).onConflictDoUpdate({
        target: devices.id,
        set: { pushToken, lastSeenAt: Date.now() },
      }),
    ]);

    return Response.json({ jwt, userId: user.id });
  },
});

这只是一个端点。认证、会话持久化和设备注册在一个 D1 事务中完成。无 Node、无独立认证服务、无 Lambda 冷启动。

代码:经由 Durable Object 的推送通知分发

移动端后端的实时部分是推送通知分发。模式:每个用户一个 Durable Object 保存该用户的在线设备列表,并向所有设备分发推送通知。

typescript
// app/server/push.ts — 每个用户一个 Durable Object 用于推送路由
import { DurableObject } from 'cloudflare:workers';

interface Device {
  id: string;
  pushToken: string;
  platform: 'ios' | 'android';
  lastSeenAt: number;
}

export class PushRouter extends DurableObject {
  devices: Map<string, Device> = new Map();

  async registerDevice(device: Device): Promise<void> {
    this.devices.set(device.id, device);
    // 持久化到 D1 用于冷启动恢复
    await this.env.DB.prepare(
      'INSERT OR REPLACE INTO devices (id, push_token, platform, last_seen_at) VALUES (?, ?, ?, ?)'
    ).bind(device.id, device.pushToken, device.platform, Date.now()).run();
  }

  async dispatch(payload: { title: string; body: string; data?: unknown }): Promise<void> {
    const routes: Promise<Response>[] = [];

    for (const device of this.devices.values()) {
      if (device.platform === 'ios') {
        routes.push(this.sendAPNs(device.pushToken, payload));
      } else {
        routes.push(this.sendFCM(device.pushToken, payload));
      }
    }

    await Promise.allSettled(routes);
  }

  private async sendAPNs(token: string, payload: any): Promise<Response> {
    return fetch('https://api.push.apple.com/3/device/' + token, {
      method: 'POST',
      headers: {
        authorization: `bearer ${this.env.APNS_KEY}`,
        'apns-topic': this.env.APNS_BUNDLE_ID,
        'apns-push-type': 'alert',
      },
      body: JSON.stringify({ aps: { alert: payload } }),
    });
  }

  private async sendFCM(token: string, payload: any): Promise<Response> {
    return fetch('https://fcm.googleapis.com/fcm/send', {
      method: 'POST',
      headers: {
        authorization: `key=${this.env.FCM_KEY}`,
        'content-type': 'application/json',
      },
      body: JSON.stringify({ to: token, notification: payload }),
    });
  }
}

Durable Object 在内存中维护设备映射,冷启动时从 D1 恢复数据,并将推送并行分发至 APNs 和 FCM。移动端客户端通过 WebSocket 连接该 Durable Object,使用户的在线设备列表保持同步,无需轮询。

Stripe(或其他第三方支付处理器)的 Webhook 接收器是另一个 Worker,向 D1 队列表写入数据;Durable Object 消费队列并分发推送。这种分离意味着 Stripe 故障不会阻塞推送分发,反之亦然。各组件独立故障。

生产环境中各组件如何协同

移动端后端有四个请求入口点,均经由同一 Cloudflare Worker 路由,但执行不同的绑定:

  1. 移动端 API 请求 — /api/mobile/* 下的 HTTPS REST 端点,由 TanStack Start Server Functions 处理,读写 D1 和 KV。
  2. Stripe Webhook — /api/webhooks/stripe 的 POST 端点,基于幂等键,由 Durable Object 强制实现恰好一次处理,向 D1 写入。
  3. WebSocket 连接 — /api/mobile/ws,升级为每个用户一个 Durable Object,分发推送通知和实时状态。
  4. 静态资源(移动端后端较少见) — R2 支持的可下载报告、导出 CSV、生成 PDF 等静态文件。

四个入口点共享同一 Cloudflare Worker 代码库和同一套绑定(D1、R2、KV、Durable Objects)。代码库是一个 TypeScript 项目;wrangler.toml 声明一次绑定即可。没有独立的"WebSocket 服务"或"API 服务"——只有一个 Worker 和多个路由处理器。

成本计算:日均百万请求,月费 $5

日均 110 万请求峰值时的托管账单(2026 年 4 月):

项目使用量费用
Cloudflare Workers 付费计划3300 万请求/月$5.00
Cloudflare D12500 万次读,20 万次写$0.00(含)
Cloudflare KV300 万次读,10 万次写$0.00(含)
Cloudflare R2存储 8GB,流量 2TB$0.12
Cloudflare Durable Objects80 万请求$0.15
APNs / FCM 推送20 万次推送$0.00
合计$5.27/月

主要成本线是 Workers 付费计划 $5/月,覆盖每月最多 3000 万请求。超出后 Workers 按每百万 $0.30 收费。按每月 1 亿请求计算,账单为 $26/月。仍比一台全天候运行 t3.medium EC2 实例便宜。

移动端客户端本身不产生 Workers 成本——只有 API 调用才计费。非活跃用户(任何移动端 App 中的大多数)成本恰好为 $0。

2025 年 10 月至 2026 年 4 月观察到的完整发票规模曲线:

月份日均请求WorkersD1KVR2DO合计
2025 年 10 月1.2 万$0$0$0$0.03$0$0.03
2025 年 12 月18 万$0$0$0$0.06$0.04$0.10
2026 年 2 月62 万$0$0$0$0.09$0.09$0.18
2026 年 4 月110 万$5.00$0$0$0.12$0.15$5.27

非线性的成本曲线(流量增长 3 个数量级,成本增长约 175 倍)才是核心结果。按此曲线扩展的 Node 服务器到 2026 年 4 月成本约 $80-$120/月(两台 t3.medium 实例加 RDS Postgres)。成本差足够资助一年的增量产品开发。

诚实的取舍:Workers 在移动端后端的劣势

我不会假装 Workers 在所有移动端后端对比中都胜出。以下四个场景你应该选择其他技术栈:

  • 长时间 CPU 计算。 Workers 对单次调用的实时钟限制为 30 秒(免费计划 15 秒)。如果你的移动端 API 需要做 ML 推理、视频转码或任何超过 30 秒的 CPU 密集型任务,你需要不同的运行时。Workers AI 在 30 秒内完成大多数推理任务;其他则需要队列加专用计算。
  • 内存密集型有状态工作负载。 每个隔离环境 Worker 内存限制为 128MB。如果你的 API 在内存中维护大型缓存、运行重型 ORM 或处理大型载荷,你会触及上限。解决方案是通过 Streams API 流式传输大响应,并将重型处理移出请求路径。
  • iOS NSURLSession 在边缘的特殊行为。 使用 URLSession 的 iOS 客户端有时会在失败后以 HTTP/1.1 回退重试请求,这可能在 /checkout 类端点上导致意外的重复 POST。修复方案是在每个状态变更端点上加幂等键请求头,并在 D1 中建立以该请求头为键的幂等表。
  • 有严格监管数据驻留要求的 App。 Workers 运行在 Cloudflare 边缘网络(330+ 个城市)。如果你需要数据保留在特定司法管辖区(仅欧盟、仅美国),Workers 默认路由可能无法满足合规要求。Workers 付费用户可以为每条路由配置区域过滤,但这是配置开销,而非默认行为。

关于与 Supabase 和 Firebase 作为移动端后端的更深入对比,参见我们的 TanStack Ship vs Supastarter 和 TanStack Ship vs Makerkit 详细分析。

决策树:何时用 Workers、何时用 Supabase、何时用裸 Node

选择 Cloudflare Workers + D1/R2/KV/DO 如果:

  • 你的移动端 API 是请求/响应或 WebSocket 模式,响应时间在 30 秒以内
  • 你的用户全球分布,边缘延迟很重要
  • 你的预算在低规模时紧张,希望成本增长可预测
  • 你不需要 Node 专属包或长时间 CPU 计算

选择 Supabase 或 Firebase 如果:

  • 你需要托管 Postgres(Supabase)或 Firestore(Firebase)并有针对移动端平台的原生 SDK
  • 你希望无需编写 WebSocket 胶水代码就能获得实时订阅
  • 你的团队已熟练掌握其中一个生态系统

选择 裸 Node(Express、Fastify、Hono)部署在 Fly.io 或 Railway 如果:

  • 你需要的包依赖 Node 内置模块(用于图片处理的 sharp、原生绑定)
  • 你的 API 有长时间流式响应
  • 你愿意自己管理服务器并希望完全掌控

关于这些技术栈之间的定价对比,参见 TanStack Ship 定价页面。

常见问题

Cloudflare Workers 支持移动端 App 的 WebSocket 吗? 支持。Workers 上的 WebSocket API 已稳定,支持双向实时消息。移动端客户端在 iOS 上使用 URLSessionWebSocketTask,在 Android 上使用 OkHttp WebSocket。参见 WebSocket 文档。

D1 能处理多个移动端客户端的并发写入吗? 能,但有注意事项。D1 使用 SQLite 的 WAL 模式,按数据库序列化写入。对于高写入工作负载(持续 >1k 次写入/秒),你需要跨多个 D1 数据库分片或迁移到不同的存储层。对于大多数移动端后端,远低于 1k 次写入/秒,D1 完全够用。

推送通知可靠性如何? APNs 和 FCM 均保证至少一次投递,但不保证最多一次。重复推送会发生;移动端客户端必须根据载荷中的 notificationId 字段做去重。上面 Durable Object 模式通过跟踪每个设备最后投递的 notificationId 在服务端做去重。

如何处理移动端 App 的令牌刷新? 签发一个短效(15 分钟)访问 JWT 和一个长期(30 天)刷新令牌。将刷新令牌存储在 iOS Keychain 或 Android EncryptedSharedPreferences 中。刷新端点根据 D1 会话表验证刷新令牌,轮换它,并签发新的访问 JWT。标准 OAuth 2.0 模式,干净地适配 Worker。

Cloudflare 出现故障时会发生什么? Cloudflare 状态页面显示 Workers 历史可用性超过 99.99%。对于关键任务移动端 App,答案是多区域兜底:将 API 复制到 AWS Lambda 或 Fly.io,并通过 DNS 健康检查路由。我目前还没有为任何移动端 App 实现这一点,因为成本收益尚未倾向于此。

使用 Workers 需要 TanStack Start 吗,还是可以用纯 Workers? 纯 Workers(fetch 处理器 API)适合任何后端。TanStack Start 是路由、服务器函数和端到端类型的约定框架。如果你团队偏好 Hono 或 itty-router,效果同样好。参见 Workers 文档 了解最小配置。

如何在边缘计算层做限流? 每个 Worker 路由通过一个滑动窗口计数器在 Durable Object 中强制执行 per-IP 和 per-user 限流。限制每 60 秒重置一次。默认限制是认证端点每用户 100 请求/分钟,未认证端点每 IP 30 请求/分钟。超出限制后 Worker 返回 429 并带 Retry-After 请求头。无需外部 API 网关。

移动端 API 的分析和可观测性如何做? Workers Analytics Engine(2026 年仍处于 Beta)是低成本方案:每请求调用一次 writeDataPoint(),每百万数据点摄入 $0.05。深度追踪方面,Cloudflare Logs 可流式传输至 R2,在开发期间用 wranger tail 解析,或在故障期间接入第三方 APM。避免记录大型请求/响应体——如果缓冲到内存中,每个日志字节都计入 Workers 打包体积限制。

移动端后端能连接到 VPN 后面的内部服务吗? Workers 无法直接访问私有 VPC。对于需要调用 AWS 或 GCP 上内部 API 的移动端后端,标准模式是通过公共互联网发起出站 HTTP 调用并使用 mTLS 认证。Cloudflare 的服务绑定允许一个 Worker 直接调用另一个 Worker 而无需经过公共互联网,这对将移动端后端拆分为多个 Workers 很有用。

如果我不再适合用 Workers,迁移路径是什么? 我见过的两条可行生产路径:(1) 将重型状态从 Durable Objects 迁移到托管 Postgres(Neon、Supabase),保留 Workers 作为 API 层;(2) 当你需要长时间 CPU 计算或 >128MB 内存时,将整个技术栈迁移到容器化运行时(Fly.io、Railway)。Workers 上 TanStack Start 代码库干净地适配这两个目标,因为 Server Functions 模式是可移植的——仅绑定声明需要更改。

结语

基于 Cloudflare Workers 的移动端后端是真实可用的生产级方案,成本极低。架构是四个绑定——D1、R2、KV、Durable Objects——每个路由组对应一个 Worker。冷启动 p99 50ms。日均 110 万请求时月费 $5.27。七个月单人独立开发,从未开过 pager。

如果你本季度要启动一个移动端 SaaS 且你的后端适配请求/响应或 WebSocket 模式,克隆 TanStack Ship,不到一小时就能让移动端认证端点跑起来。四个绑定和部署流水线已经接好。