DeepSeek API 报 429 怎么办:并发限流不是配额用完
数据截至 2026-07,价格与限额以各官网为准。
DeepSeek API 返回 429 的时候,绝大多数情况不是你的钱花完了,而是同一时刻在途的请求太多,撞上了并发上限。 这两件事的处理方式完全相反:配额问题要去充值或等重置,并发问题只需要在客户端把”同时发出去的请求数”压下来,代码里加十几行就能解决,一分钱不用多花。分不清这两者,就会出现”我明明还有余额,为什么一直 429”的困惑,然后白白去翻账单页面。
先把这个误解掰过来:429 不等于”没额度了”
HTTP 429 的标准语义是 Too Many Requests,字面意思是”请求太多”,不是”你没钱了”。很多人被别的平台的习惯带偏了——有些服务确实会在余额不足时返回 429,但 DeepSeek 官方文档里对限流的表述是并发数:deepseek-v4-flash 为 2500 并发,deepseek-v4-pro 为 500 并发。
注意”并发”这个词的准确含义:它指的是同一时刻正在处理中、还没返回结果的请求数量,不是”每分钟能发多少次”。这两个口径差别很大。举个例子,如果每个请求平均耗时 2 秒,你用 500 的并发跑满,一分钟其实能完成大约一万五千次请求;反过来,如果你的请求是长上下文推理、单次要跑三十秒,那 500 并发一分钟也就千次左右的吞吐。所以”我一分钟才发了几百个请求怎么会限流”这种推理是不成立的——决定你会不会撞墙的是在途请求数,不是发送频率。
也正因如此,最容易撞 429 的场景往往不是高流量线上服务,而是本地跑批:写个脚本把两千条数据一股脑丢进线程池,前几十个还好好的,剩下的全在报错。线上服务反而因为请求是零散到达、平均在途数量可控,很少撞到。
另外要说明的是,除并发之外官方是否还有其他维度的限制(比如按分钟计的请求数或 token 数),这篇不替官方下结论,以你调用时 api-docs.deepseek.com 上的当前页面为准。这篇只依据官方明确标注过的并发口径来给方案。
第一步:先确认是不是真的限流
在动手改代码之前,花两分钟排除掉几种”长得像 429,其实不是限流”的情况,否则你限了半天并发问题依然在。
- 看响应体,别只看状态码。HTTP 客户端库经常只把 status code 抛出来,真正的错误描述在 body 里。用
curl -i或者在代码里把原始响应打印出来,看清楚服务端到底说了什么,是并发超限、鉴权问题还是余额问题。 - 确认请求真的打到了 DeepSeek。base_url 应该是
https://api.deepseek.com。如果你是从别的兼容服务改过来的,或者中间挂了代理、网关,429 有可能是中间那一层限你的,跟 DeepSeek 一点关系没有。这种情况在公司内部统一网关的场景里非常常见,排查时先绕过网关直连试一次,结论立刻就清楚了。 - 确认不是模型名的问题。旧的
deepseek-chat/deepseek-reasoner两个名字官方标注在 2026 年 7 月 24 日 15:59 UTC 弃用,新代码请直接写deepseek-v4-flash或deepseek-v4-pro。模型名不对通常报的是别的错,但排查期一次性把它核对掉,能省掉后面反复怀疑的时间。这块细节可以看 deepseek-chat / deepseek-reasoner 报 404 了?。 - 确认密钥没有被别人共用。并发是按账号算的,不是按你这台机器算的。如果同一把 key 同时被测试环境、同事的脚本、某个忘了关的定时任务在用,你自己这边看着只发了几十个请求,账号层面早就满了。密钥在 https://platform.deepseek.com/api_keys 管理,建议按用途拆成多把,出问题时能定位到具体是谁在占。
第二步:用信号量把在途请求数摁住
确认是并发限流之后,正解不是”重试到成功为止”,而是从源头上不让在途请求数超过阈值。重试是兜底,限流才是主动控制。
Python 里最简单的做法是用信号量。同步版本配合线程池:
import os
import threading
from concurrent.futures import ThreadPoolExecutor
from openai import OpenAI
client = OpenAI(
api_key=os.environ.get("DEEPSEEK_API_KEY"),
base_url="https://api.deepseek.com",
)
# 这个数字自己定,务必留出余量,不要贴着官方上限跑
MAX_INFLIGHT = 50
sem = threading.Semaphore(MAX_INFLIGHT)
def ask(prompt: str) -> str:
with sem: # 拿不到令牌就在这里排队,而不是把请求打出去
resp = client.chat.completions.create(
model="deepseek-v4-flash",
messages=[{"role": "user", "content": prompt}],
)
return resp.choices[0].message.content
with ThreadPoolExecutor(max_workers=MAX_INFLIGHT) as pool:
results = list(pool.map(ask, prompts))
异步版本同理,把 threading.Semaphore 换成 asyncio.Semaphore,with 换成 async with 就行。asyncio 场景下更要留意这一点:协程创建成本极低,一个 asyncio.gather 轻轻松松就能同时发出去几千个请求,没有信号量的话撞墙几乎是必然的。
MAX_INFLIGHT 该设多少?给一个务实的思路:从小往大调,而不是从官方上限往下退。先设个几十跑通,观察实际吞吐够不够用,不够再往上加。理由有三个:一是你的密钥可能被多处共用,官方上限不等于你一个人能用的量;二是本机的网络连接数、内存、下游数据库往往比 API 先成为瓶颈;三是贴着上限跑意味着任何抖动都会立刻变成错误。留出余量的代价只是吞吐略低,撞墙的代价是整批任务需要重跑。
第三步:退避重试怎么写才不添乱
限住并发之后仍然可能偶发 429,这时候需要重试兜底。但重试写不好会放大问题——最典型的反面教材是”失败了立刻重发”,一批请求同时失败又同时重发,等于把冲击又完整地打了一遍。
正确的姿势是指数退避加随机抖动:
import random
import time
def ask_with_retry(prompt: str, max_retries: int = 5) -> str:
for attempt in range(max_retries):
try:
return ask(prompt)
except Exception as e:
if not _is_rate_limited(e) or attempt == max_retries - 1:
raise
# 1s、2s、4s、8s… 再叠加随机抖动,避免所有请求同时回来
delay = (2 ** attempt) + random.uniform(0, 1)
time.sleep(delay)
几点说明:
- 退避的基数和上限是经验值,不是官方规定,上面写 1 秒起步、每次翻倍纯粹是常见做法,你可以按自己的场景调。如果响应头里带有服务端给出的等待时长提示,优先按它来,具体有没有这个字段、叫什么名字以官方文档当次页面为准,别照抄别的平台的字段名。
- 随机抖动那一项不要省。同一批同时失败的请求如果退避时长完全一致,它们会在同一时刻再次涌回来,等于什么都没改善。
- 只对限流类错误重试。参数错误、模型名不存在、鉴权失败这些重试一百次也是同样的结果,只会浪费时间。上面代码里的
_is_rate_limited需要你按实际拿到的异常类型和响应内容去判断,不要无脑except全部重试。 - 重试次数要有上限。无限重试会让一个本该快速失败的任务挂在那里,掩盖真正的问题。
别忽略缓存:它能顺带缓解压力
DeepSeek 有一个叫上下文硬盘缓存的机制,官方文档标注默认对所有用户开启,不需要改代码。它的直接价值是省钱:以官方人民币定价看,deepseek-v4-flash 缓存命中的输入价是 0.02 元/百万 tokens,未命中是 1 元/百万 tokens;deepseek-v4-pro 分别是 0.025 元和 3 元/百万 tokens。差距是数十倍量级。
跟 429 的关系在于:命中缓存的请求,模型不需要重新处理那部分前缀,单次请求的处理时间通常会更短,在途时间短了,同样的并发额度下能过的请求自然更多。所以把提示词组织成”固定前缀在前、变化内容在后”的结构,既省钱也间接缓解并发压力。
要注意几个前提,别抱不切实际的期待:
- 缓存只匹配输入的前缀部分,输出仍然要模型实际生成,不受缓存影响,随机性也不变。
- 缓存是尽力而为的,官方明确不保证 100% 命中。
- 不使用的缓存会自动清理,官方原文的说法是”一般数小时至数天”。
- 想确认到底命中没有,看返回
usage里的prompt_cache_hit_tokens和prompt_cache_miss_tokens两个字段,别靠感觉判断。
关于计费的完整说明,可以看 DeepSeek API 怎么收费。另外网上有关于峰谷时段差异定价的说法,这篇不采信未在官方定价页正式公示的数字,以你查看时 api-docs.deepseek.com 和 platform.deepseek.com 页面显示的为准。
还有哪些减压手段
除了限并发和用缓存,下面这些也值得考虑,按投入产出排序:
- 该用 Flash 就别上 Pro。官方标注的并发额度本身就不同,Flash 是 2500、Pro 是 500,差了五倍。分类、抽取、格式转换这类任务用 Flash 通常够用,把 Pro 留给真正需要深度推理的环节,等于凭空多出好几倍的并发空间,顺带还省了钱。
- 合并请求。一次调用里处理十条数据,比拆成十次调用更省并发额度。当然要权衡:合并后单次输出变长、在途时间也变长,而且一条出错可能拖累整批,需要按实际数据量试。
- 在队列层削峰。如果业务本身是异步可延迟的,把任务放进队列由固定数量的 worker 消费,是比在调用点做信号量更结构化的做法,也更容易观测和调整。
- 加监控。至少记录三个数字:当前在途请求数、429 出现次数、平均单次耗时。没有这三个数,调并发参数就是纯猜。
什么情况下这些办法不管用
诚实说几个不适用的场景,免得你在错误的方向上耗时间。
如果你的 429 是在第一个请求就出现的,那基本可以排除并发问题,去查密钥状态、账户余额和请求本身。如果是通过第三方中转或聚合平台调用的,那么限流规则由那个平台自己定,跟 DeepSeek 官方标注的并发数没有关系,这时候官方文档帮不了你,得看你用的平台自己的说明——相关取舍可以看 API 聚合与中转平台怎么选。如果你限并发之后吞吐达不到业务要求,那问题的性质已经变了,不再是”怎么绕开 429”,而是”单请求耗时能不能优化”或”要不要拆多账号、走更高档位”,后者需要跟官方渠道确认政策,不是客户端代码能解决的。
还有一种情况:你的请求本身就是超长上下文。官方标注 V4 系列上下文窗口 1M tokens、最大输出 384K tokens,能力上撑得住,但这类请求单次在途时间长,会长时间占着并发名额。如果它们和大量短请求混在一起跑,短请求会被拖累。做法是把长短请求分开跑,各自设不同的并发上限,别放在同一个池子里抢。
小结
DeepSeek 的 429 绝大多数是并发限流,跟余额没关系,官方标注的口径是同时在途请求数:Flash 2500、Pro 500。先用 curl -i 看清响应体、排除网关和共用密钥的干扰,确认确实是限流再动手。核心解法是客户端信号量,从几十开始往上调,别贴着官方上限跑。退避重试只做兜底,必须带随机抖动、必须只对限流错误重试、必须有次数上限。顺手把提示词的固定前缀提到最前面吃上缓存,既省钱也缩短在途时间。真到了限并发也满足不了吞吐的地步,那就是架构问题而不是报错问题了,得换个思路解。