在对接 WhatsApp Cloud API 的过程中,一旦业务进入放量阶段,"发送过快被限流"几乎是一道必答题。很多团队一开始采用简单的循环调用,结果在活动或用户唤醒场景下触发平台限速,导致消息大量失败、队列堆积,甚至影响正常会话。本文从实际工程经验出发,分享一套基于令牌桶与指数退避的多账号限速方案。
核心结论
- WhatsApp Cloud API 对每条商业账号(WABA)有单位时间调用上限,超过限制时返回 429 或特定错误码;
- 单账号串行发送无法支撑高并发,必须在多账号之间做配额分配;
- 令牌桶控制瞬时流量,指数退避处理偶发限流,二者结合才能兼顾吞吐与稳定;
- 建议在应用层记录每个账号的剩余配额与最近一次 429 时间,避免盲目重试。
一、为什么要做应用层限流
WhatsApp Cloud API 的限流策略通常以"业务账号 + 时间窗口"为维度。当某条消息通道在 1 秒内请求过多时,平台会返回类似 131056 或 HTTP 429 的响应。此时如果客户端继续暴力重试,不仅无法提升成功率,还可能拉长封禁窗口。
在我们的业务场景中,曾遇到过这样的问题:
- 凌晨批量推送时,单账号瞬时 QPS 达到平台阈值的两倍以上;
- 失败后立即重试,导致同一批消息反复占用配额;
- 多账号之间没有协调,热门账号最先被打满,冷门账号却闲置。
这些问题说明,仅靠平台侧的被动限流是不够的,应用层必须主动做流量塑形。
二、令牌桶:平滑瞬时流量
令牌桶是限流领域最经典的算法之一。它的核心思想是:以固定速率向桶中放入令牌,每次发送消息前先从桶中取走一枚令牌;无令牌时请求进入等待或降级逻辑。
2.1 单账号令牌桶实现
以下是一个基于 Python 的简化实现,支持按账号维度限速:
python
import time
import threading
from collections import deque
class TokenBucket:
def __init__(self, rate: float, capacity: int):
self.rate = rate
self.capacity = capacity
self.tokens = capacity
self.last_update = time.monotonic()
self.lock = threading.Lock()
def consume(self, tokens: int = 1, timeout: float = None) -> bool:
with self.lock:
now = time.monotonic()
elapsed = now - self.last_update
self.tokens = min(self.capacity, self.tokens + elapsed * self.rate)
self.last_update = now
if self.tokens >= tokens:
self.tokens -= tokens
return True
if timeout is None:
return False
# 简单阻塞等待
sleep_time = (tokens - self.tokens) / self.rate
if timeout and sleep_time > timeout:
return False
time.sleep(sleep_time)
return self.consume(tokens, timeout=None)
在这个实现中,rate 表示每秒生成的令牌数,capacity 表示桶的最大容量。调用 consume() 时,如果令牌不足,可以选择立即失败或阻塞等待。
2.2 多账号配额池
当系统中存在多个 WhatsApp Business 账号时,每个账号都应该有自己的令牌桶。业务层根据消息优先级、账号健康度动态选择发送通道:
python
buckets = {
"waba_01": TokenBucket(rate=80, capacity=100),
"waba_02": TokenBucket(rate=80, capacity=100),
"waba_03": TokenBucket(rate=80, capacity=100),
}
def pick_account() -> str:
# 优先选择令牌充足且近期未触发 429 的账号
healthy = [
aid for aid, b in buckets.items()
if b.tokens > 5 and not is_recently_limited(aid)
]
return max(healthy, key=lambda aid: buckets[aid].tokens, default=None)
三、指数退避:处理偶发限流
令牌桶解决的是"正常流量下的平滑"问题,但平台限流策略可能因突发流量、账号状态变化等原因临时收紧。此时需要用指数退避来冷却请求。
3.1 基础退避策略
python
import random
MAX_RETRIES = 5
BASE_DELAY = 1.0
async def send_with_backoff(message, account_id):
for attempt in range(MAX_RETRIES):
try:
return await whatsapp_api.send(message, account_id=account_id)
except RateLimitError as e:
delay = min(BASE_DELAY * (2 ** attempt), 60)
jitter = random.uniform(0, 0.3 * delay)
await asyncio.sleep(delay + jitter)
raise RetryExhaustedError(f"account={account_id}, message={message.id}")
这里有两个关键点:
- 指数增长:第一次等 1 秒,第二次等 2 秒,第三次等 4 秒,避免固定间隔造成二次冲击;
- 抖动(jitter):在同批消息被限流时,抖动可以让它们在时间轴上散开,降低同时重试的概率。
3.2 全局限流感知
除了单条消息的退避,还需要在账号维度维护一个"限流冷却期"。一旦某个账号收到 429,就在未来 30 秒到 2 分钟内降低其优先级:
python
limited_accounts = {}
async def send(message):
account_id = pick_account()
if account_id is None:
# 所有账号都处于冷却期,先入队
message_queue.append(message)
return
try:
result = await send_with_backoff(message, account_id)
return result
except RetryExhaustedError:
limited_accounts[account_id] = time.monotonic() + 120
message_queue.append(message)
四、队列与降级设计
高并发场景下,仅仅"限速"还不够,必须有队列承接暂时发不出去的消息。推荐采用分级队列:
- P0 队列:用户主动触发的会话消息,需要尽快送达;
- P1 队列:营销或批量通知,允许一定延迟;
- P2 队列:可降级的非关键消息,在高峰期可以直接丢弃或延后。
WADesk 在实际产品中采用了类似的队列分级策略,将不同业务类型的消息分配到不同通道,并通过后台面板实时监控各账号的令牌余量、队列长度与平均耗时。
五、可观测性不可忽视
限流方案上线后,建议关注以下指标:
| 指标 | 含义 | 建议阈值 |
|---|---|---|
| 429 发生率 | 单位时间内被限流的比例 | < 1% |
| 平均退避次数 | 单条消息平均重试次数 | < 1.5 次 |
| 队列积压深度 | 等待发送的消息数量 | 按业务设定 |
| 账号令牌利用率 | 实际消耗 / 配额上限 | 70% ~ 85% |
通过 Prometheus + Grafana 或类似组合,可以将上述指标可视化。一旦发现 429 发生率持续升高,就需要检查配额配置、账号健康度或业务突发流量来源。
六、常见踩坑点
- 忽略错误码细分 :WhatsApp Cloud API 的 429 响应中通常包含
retry_after字段,直接读取它比固定退避更精准; - 多进程重复初始化令牌桶:如果用多进程部署,每个进程都维护独立桶会导致实际流量翻倍,应使用 Redis 等共享存储;
- 退避时间过长影响用户体验:营销消息可以适当延迟,但会话类消息需要设置更短的上限;
- 只限流不扩容:在账号数量不足时,限流只能延缓问题,不能解决根本吞吐瓶颈。
七、总结
WhatsApp Cloud API 的限流不是一道"能不能发"的问题,而是"怎么稳定地发"的问题。令牌桶负责日常流量塑形,指数退避负责应对突发限流,多账号配额池负责横向扩展,分级队列负责业务隔离。三者组合,才能在不影响用户体验的前提下,支撑大规模消息发送。
如果你的团队正在从"能发就行"走向"高并发、可观测、可降级",建议先把单账号限速跑通,再逐步扩展到多账号调度。WADesk 的多账号消息路由模块也提供了类似的流量控制能力,感兴趣的同学可以把它作为参考实现来理解工程化细节。