App Store Connect API 限流时,不要用无限重试硬顶。先把上传请求、状态查询和 Apple 后台 Processing 拆开,再用指数退避、幂等任务、Webhook 触发和有限次数补偿查询恢复流程;如果 API 状态管理阻塞了二进制交付,就让 Transporter 或 Xcode 负责上传,API 只负责后续状态管理。

这篇文章适合三类人:在远程 Mac 上自动上传 TestFlight 构建的独立开发者;多个 App 或 Runner 共用 API Key 的小型团队;正在维护 fastlane、脚本或 CI 发布流程,并希望保留完整状态记录的自动化维护者。

01

先判断:这是 API 限流,还是上传与 Processing 延迟

“构建没有出现”不是一个足够准确的故障结论。你需要先看证据入口,再决定下一步动作。

App Store Connect API 官方说明,API 会通过 X-Rate-Limit 响应头返回限流信息,响应中可能包含每小时限制和剩余请求量;如果超过限制,API 会返回 HTTP 429,错误 code 为 RATE_LIMIT_EXCEEDED。这里的时间窗口是滚动小时,具体额度可能变化,不能把文档中的示例额度当作你的固定配额。参考 Apple 的限流识别文档。

故障现象 证据入口 下一步动作
API 请求返回 429 或 RATE_LIMIT_EXCEEDED HTTP 状态、错误 code、X-Rate-Limit 暂停当前请求,记录任务状态,进入退避队列
Transporter 报告交付失败 Transporter 交付日志、文件校验和、错误详情 修复构建或上传链路,不要只重试 API 查询
上传完成但 App Store Connect 仍显示 Processing Build Uploads 记录、页面状态、Webhook 事件 等待后台处理,避免重复上传同一构建
API 查询成功但 TestFlight 不可测试 Build 状态、Beta 状态、合规状态 分开确认构建处理、出口合规与测试可用性
多个 Runner 同时查询同一 Build 远程 Mac 任务日志、Job ID、请求路径 合并任务,限制同一资源的并发查询

二进制已经交付,不代表构建已经处理完成,也不代表它已经可以在 TestFlight 中测试。Apple 的上传说明明确区分了上传过程与后续处理;构建需要经过 Apple 系统处理后,才会出现在 App Store Connect 中。参考 Apple 的上传构建说明。

Apple 对上传状态也有独立定义:Processing 表示仍在处理,Failed 表示处理完成但发现问题,Complete 表示上传处理成功并可用于测试。如果上传长期停留在 Processing,应该查看交付详情并通过官方渠道反馈,而不是让脚本无限轮询。相关状态定义见 Build Upload Statuses 官方说明。

02

App Store Connect API 限流的真正放大器是重复轮询

很多限流事故不是一次上传产生了大量请求,而是多个自动化环节重复确认同一个结果。

常见放大方式包括:

  • 固定间隔查询,没有读取 X-Rate-Limit 剩余量。
  • 上传脚本、后台 Worker 和通知服务同时查询同一个 Build。
  • 任务失败重启后,没有读取持久化状态,重新创建上传任务。
  • 一个 Job 查询上传状态,另一个 Job 查询 Build 状态,第三个 Job 又扫描 TestFlight 列表。
  • API 返回暂时性错误后,所有 Runner 同时立即重试。
  • 任务只保存“成功”或“失败”,没有保存“已上传、等待处理、待确认”等中间状态。

在 Apple 的资源模型中,Build Uploads 代表上传操作,Build 资源则用于读取成功上传后的构建信息;这两个阶段不应由同一个无限循环混在一起处理。参考 Build Uploads API 资源说明。

一个更稳妥的任务记录可以长这样:

task_id: <TASK_ID>
app_id: <APP_ID>
bundle_id: <BUNDLE_ID>
version: <VERSION>
build_id: <BUILD_ID>
key_id: <KEY_ID>
issuer_id: <ISSUER_ID>
upload_state: uploaded
processing_state: waiting
last_http_status: 202
last_request_id: <REQUEST_ID>
last_confirmed_at: <TIMESTAMP>
runner_host: <HOSTNAME>
log_path: <REDACTED_LOG_PATH>

这些字段不是为了增加日志量,而是为了让任务重启后能够回答一个关键问题:原来的上传到底走到了哪一步?

⚠️ 注意:不要把 429 简化成“文件上传失败”。它可能只发生在上传完成后的状态查询阶段。先看 Transporter 交付记录和 Build Uploads 状态,再决定是否需要重新上传。

03

把上传、确认、Processing 和 TestFlight 拆成四层

远程 Mac 自动上传最容易出错的地方,是把“执行上传”和“确认可测试”写成一个长脚本。只要其中一个查询接口暂时失败,脚本就可能误判为整体失败。

层级 负责什么 能证明什么 不能证明什么
上传执行 Xcode、Transporter 或上传工具提交二进制 文件已开始或完成交付 构建已经处理完成
上传结果确认 读取交付记录、Build Uploads 状态 上传操作是否成功、是否失败 TestFlight 是否已经可测试
后台 Processing 查询 读取构建或上传处理状态 Apple 是否完成后台处理 所有测试资格和合规条件
最终可测试确认 检查 Build 状态、Beta 状态和必要合规项 测试人员是否能使用该构建 下一次上传一定不会失败

Apple 的 Build Uploads 文档提供了 BUILD_UPLOAD_STATE_UPDATED Webhook 事件,可用于接收上传状态变化;Build 资源则用于上传成功后读取构建信息。这个边界正适合用来拆分任务队列。

因此,建议采用下面的状态流:

< TASK_ID >
    ↓
build_generated
    ↓
upload_started
    ↓
upload_completed
    ↓
waiting_for_processing
    ↓
processing_completed
    ↓
ready_for_test

任何状态都不要仅凭远程 Mac 上的进程退出判断。SSH 断线、终端关闭、Transporter 日志延迟,都可能让本地进程状态与 Apple 端状态不一致。

04

退避、幂等与 Webhook 组成恢复策略

收到 429 后,第一步不是把重试次数调大,而是把当前请求变成一个可恢复任务。

退避策略

你的重试策略至少需要具备以下条件:

  • 根据 HTTP 状态和 API 错误 code 区分限流与业务错误。
  • 将任务放回队列,而不是在当前进程中阻塞等待。
  • 使用递增退避,并加入随机扰动,避免多个 Runner 同时再次发送。
  • 每次重试前重新读取任务状态。
  • 达到内部上限后停止自动化,进入人工接管。
  • 将最后一次响应、请求路径和资源 ID 写入日志。

Apple 官方建议在定期检查某个值时进行节流,并在遇到 429 时记录失败、稍后重新排队处理。官方没有为所有端点规定一个统一的轮询间隔,所以不要在脚本中写死一个适用于所有项目的固定秒数。参考 限流识别与处理建议。

幂等策略

上传限流后是否要重新上传,取决于失败发生在哪一层:

✅ 如果 Transporter 已报告文件交付完成,先确认原始上传记录和 Build ID。
✅ 如果只是 API 查询返回 429,保留原任务,不要创建新上传。
✅ 如果上传状态明确为 Failed,先查看错误详情,再判断是否修复后重传。
❌ 不要因为远程 Mac 的 SSH 会话断开,就直接重新生成构建。
❌ 不要用新的随机任务 ID 覆盖旧任务,导致多个任务同时追踪同一个版本。

Webhook 策略

Webhook 更适合承担“事件通知”,不适合替代所有状态读取。Apple 的 Webhook 机制通过 HTTP POST 向你的端点发送事件;你收到通知后,应先验证 x-apple-signature,再根据事件中的资源标识执行定向查询。参考 Webhook 配置与解析文档。

可优先关注构建上传状态变化事件。Apple 的事件类型文档列出了 BUILD_UPLOAD_STATE_UPDATED,同时也提供了 TestFlight 构建状态变化相关事件。参考 WebhookEventType 官方定义。

事件驱动流程可以这样设计:

收到事件
  ↓
验证签名
  ↓
根据资源 ID 查找 <TASK_ID>
  ↓
确认事件是否重复
  ↓
更新持久化状态
  ↓
必要时执行一次定向 API 查询
  ↓
通知远程 Mac 任务继续或停止

不要在 Webhook 收到后再次扫描整个 App 的所有 Build。这样做会把事件驱动流程重新变成高频轮询。

05

远程 Mac 任务需要隔离请求、凭据和日志

当多个 App 共用一台远程 Mac 时,限流问题往往不是单个项目造成的,而是共享资源没有边界。

建议按以下维度拆分:

  • 按 App 隔离:每个 App 使用独立的任务队列和状态文件。
  • 按环境隔离:测试、预发布和正式发布不能共用同一个重试队列。
  • 按阶段隔离:构建、上传、Processing 查询、TestFlight 确认分别记录。
  • 按凭据隔离:API Key、私钥文件和 Keychain 会话不能混在公共脚本目录。
  • 按主机任务隔离:同一台远程 Mac 上,为每次发布保留独立工作目录和脱敏日志。

API 的错误处理也不能只看状态码。Apple 建议结合 HTTP 状态码和错误响应中的 code 判断问题,并在可用时读取错误来源信息。参考 App Store Connect API 错误处理文档。

你至少应记录以下观测字段:

记录项 用途
请求类型与资源路径 判断是上传、Build 查询还是状态扫描
HTTP 状态与错误 code 区分限流、权限、参数和服务端错误
API Key 标识 发现多个项目共享同一限流边界
Build ID 与任务 ID 防止重复上传和交叉确认
上传状态与 Processing 状态 分开判断交付和后台处理
最后确认时间 判断任务是否真的卡住
Webhook 事件 ID 处理重复通知和补偿查询
SSH 会话与主机日志 识别远程 Mac 断线造成的误判

Apple 的 Webhook 文档还提供了历史交付记录和重新发送失败通知的能力。你可以把这些记录纳入排障,而不是只依赖远程 Mac 本地日志。

06

三种方案的取舍:继续轮询、降低频率,还是改成事件驱动

方案 适用情况 优点 风险
保留现有 API 查询 项目规模小,只有一个 Runner 修改成本低,排障直观 容易重复查询,难以扩展
降低自动化频率 偶发发布,Processing 查询量有限 可快速缓解 429 不能解决重复任务和状态丢失
Webhook 加补偿查询 多 App、多 Runner、需要持续发布 请求更集中,状态更清晰 需要维护接收端和签名校验
上传与 API 查询拆分 Transporter 上传稳定,但状态脚本经常失败 交付链路与状态链路互不阻塞 需要持久化任务状态
远程 Mac 任务分组 多项目共享主机 凭据、日志和重试边界清楚 初期配置与运维工作更多

对于独立开发者,通常不需要一开始就重写整个发布系统。先完成任务唯一标识、状态持久化和重复任务拦截,再把高频轮询改成 Webhook 加补偿查询,收益更直接。

如果你还没有稳定的远程 Mac 环境,可以先查看 VpsMesh 的 Mac 远程租赁方案,重点确认是否支持你需要的 SSH、网页控制台和持续运行方式。需要固定主机执行长期构建时,也可以对比 Mac mini M4 租赁配置。

07

用一次真实 TestFlight 上传验收修复结果

不要只用“脚本退出码为 0”验收。你需要让一条脱敏的真实发布链路完整走通。

  • [ ] 生成一个新的构建,并保存 <TASK_ID>、<BUILD_ID> 和 <VERSION>。
  • [ ] 通过 Transporter 或 Xcode 完成二进制交付,保存交付日志路径。
  • [ ] 确认上传任务状态,不把 API 查询失败误判成文件上传失败。
  • [ ] 让构建进入 Processing,并验证任务不会高频扫描同一资源。
  • [ ] 接收并验证 Webhook;若事件重复,任务状态不能重复推进。
  • [ ] 人为重启远程 Mac 上的 Runner,确认它会读取旧状态,而不是重新上传。
  • [ ] 模拟一次 429,确认任务进入退避队列并保留请求证据。
  • [ ] 最终确认 Build 达到可测试状态,再通知 TestFlight 测试人员。
  • [ ] 检查日志中没有私钥、完整 JWT、真实主机名或未脱敏请求参数。

Apple 的构建状态页说明,Complete 表示上传处理成功并可用于测试;如果状态为 Failed,应先处理错误,再重新上传。App Store Connect 的 Build 状态与上传状态不是同一层,因此验收时要分别记录。

如果这次验收成功,你的最终决策通常是三选一:继续现有方案但降低查询频率;把 Transporter 上传和 API 状态确认拆成两条链路;或者进一步改成 Webhook 触发、补偿查询兜底的事件驱动流程。

08

FAQ:限流、重复上传与远程 Mac 自动化

App Store Connect API 返回 429 后,应该先做什么?

先保存 HTTP 状态、错误 code、X-Rate-Limit 响应头、请求路径和任务标识,不要立即重复发送同一个请求。将任务放回队列,采用带随机扰动的退避策略;如果只是状态查询,优先等待 Webhook,再进行一次有条件的补偿查询。

App Store Connect API 的状态查询应该多久执行一次?

Apple 没有为所有资源公布一个通用固定轮询间隔。你应根据剩余请求量、任务阶段和最近响应动态调整频率,并限制同一 Build ID 的并发查询。后台处理期间,优先使用构建状态 Webhook,减少主动扫描。

上传触发限流后,需要重新上传 iOS 构建吗?

不一定。若 Transporter 已报告交付完成,限流只发生在后续 API 查询阶段,应先确认原上传记录和 Build ID,而不是重复上传。只有交付失败、状态明确为 Failed,或日志显示文件未完成提交时,才进入重新上传判断。

事件通知能否降低 App Store Connect API 的查询量?

可以。将构建上传和 TestFlight 状态变化交给 Webhook 通知,收到事件后只针对对应资源执行确认查询,就能避免多个 Runner 持续扫描全部构建。但 Webhook 不是完整状态数据库,仍应保存事件 ID、任务 ID和最后确认状态,并为丢失事件保留有限补偿查询。

远程 Mac 自动上传 App Store Connect 时,怎样避免重复任务?

为每次发布建立唯一任务 ID,并持久化 App ID、版本号、Build ID、上传状态和最后确认时间。任务重启后先读取状态,再决定继续查询、等待 Webhook、人工接管或重新上传,不能依据进程退出直接判断上传失败。

对比“继续用现有方案”和“换成更稳的远程 Mac 发布环境”,真正的差别不只是能不能上传。共享主机上的凭据容易混用,SSH 断线后状态容易丢失,Runner 之间也可能互相重复查询;如果本地 Mac 还需要长期占用磁盘和网络资源,维护成本会继续增加。对于需要持续在线运行、但不想专门购买一台 Mac 做打包机的独立开发者,租赁 VpsMesh 的 Mac 环境更适合先完成这类发布链路验证。建议先拆开上传与状态查询,再用一次真实 TestFlight 构建验收重试边界;只有验收结果稳定后,才值得把更多 App 接入自动化队列。