任务提交成功后,getTaskResult 一直返回 processing,常见原因主要有四类:
- 等待时间本来就还没到,Token 类任务通常需要十几秒到几十秒;
- 轮询代码有缺陷,比如没有总超时、漏处理错误字段、查错了 taskId;
- 任务参数存在“软错误”,后端无法完成挑战;
- 代理链路或目标站点出了问题。
排查建议从自己的代码查起,再核对参数,最后才怀疑代理、目标站和服务端排队。前几项最容易查,出问题的概率也最高。
不管原因是哪一类,轮询都要设总超时。按任务类型设一个上限(Token 类常见做法是 60–120 秒),到点就停止等待,记录 taskId,再按策略决定是否重新提交。
先判断:是“慢”,还是真的“卡”
createTask 返回 taskId,只说明任务已经入队,挑战还没有完成。之后任务状态在 processing 和 ready 之间切换,完成之前每次查询都会得到 processing 或类似的“未就绪”响应。
所以第一步是拿等待时间和任务类型对照:
- 图形 OCR(ImageToText 一类):通常几秒内返回。等了 30 秒还没结果,基本可以按异常处理。
- Token 类任务(reCAPTCHA v2/v3/Enterprise、Cloudflare Turnstile/Challenge、GeeTest v4、Arkose 等):后端要走完浏览器或协议层面的挑战流程,十几秒到几十秒都常见。如果按 OCR 的速度来预期,很容易误以为任务卡死了。
建议按类型分别统计等待时长的 P50 和 P95,不要只看平均值。假设某类任务的 P95 在 40 秒左右,30 秒的超时就会把一部分本来能成功的任务提前丢掉。反过来,如果 P50 从十几秒突然涨到一分钟,才说明有东西变了。
按顺序排查
1. 轮询代码本身有没有问题
不少“一直 processing”,查到最后是客户端的问题:
- 没有总超时:循环只在 ready 时退出,遇到失败或异常的任务就会一直等下去。
- 只看状态,不看错误字段:任务式接口的响应里通常同时有错误标识(errorId 一类字段)和状态。如果代码只判断“不是 ready 就继续等”,任务失败也会被当成 processing。
- 查错了任务:并发时 taskId 被覆盖、复用了旧变量,或者查询用的密钥属于另一个账户。
- 轮询太频繁:每秒查好几次可能触发频控。拿到 429 或被限流后,代码又把这类异常当成“未完成”。多家平台的文档都建议 3–5 秒查一次;有的平台还限制单个任务的查询次数,超过后可能被限流或丢弃。
- HTTP 层出错:请求超时、中间代理缓存了响应、重试库吞掉了异常,最终都会表现为“一直拿不到结果”。
检查方法:每次查询都记录原始响应体、HTTP 状态码、taskId 和时间戳。如果原始响应里其实已经带了错误信息,问题就出在你的解析逻辑。
2. 任务参数和页面是否一致
参数错误不一定在 createTask 阶段就被拒绝。有些错误要到后端真正去完成挑战时才会暴露,表现为后端反复尝试,直到平台端超时。常见情况:
- websiteKey 和页面实际加载的不一致。同一站点的不同页面、测试环境和生产环境,可能用的是不同的 key;
- websiteURL 填的是跳转前的地址,没有用重定向后实际的协议、域名和路径;
- 缺少上下文参数,例如 reCAPTCHA v3 的 pageAction、Enterprise 标记,或 Turnstile 的 action、cData;
- 任务类型选错,比如把 Enterprise 当普通 v2 提交,或者把 Cloudflare 整页 Challenge 当成 Turnstile 组件处理。
参数最好从浏览器开发者工具里的实际网络请求中提取,这比在页面源码里搜索更可靠。具体提取方法可参考 Puppeteer 验证码接口怎么接。
3. 代理链路是否正常
如果用的是自带代理的任务类型,服务端要通过你提供的代理去访问目标站点。以下情况都可能让任务长时间停在处理中,或者多次网络重试后失败:
- 代理认证失败或连接超时;
- 出口 IP 被目标站返回 403/429;
- 出口 IP 已被目标站风控。
对照方法:同一组参数,分别换一个确认可用的代理、或者改用无代理任务类型(前提是该类型支持),各提交几次。如果只有使用某个代理的任务卡住,问题就在代理。也可以先用这个代理直接打开一次目标页面,看能否正常加载。
4. 目标站变化与服务端排队
目标站更换了验证码接入方式,或者验证服务临时提高了难度(比如要求多轮选择),都会让同类任务整体变慢,服务端队列随之堆积。
这种情况的特征是:同一类型的任务,换了参数和代理也一样变慢,而其他类型正常。这时改自己的代码作用不大。应该记录时间窗口和一批 taskId 样本去联系服务商,业务侧同时降速或暂停。

一个带兜底的轮询写法
下面是通用伪代码,不能直接运行。字段名和错误标识请以你所用平台的官方文档为准:
# 伪代码:按任务类型设置等待上限,数值按自己统计的 P95 调整
DEADLINE = { 'image_ocr': 30, 'token_task': 120 } # 秒
INITIAL_WAIT = { 'image_ocr': 2, 'token_task': 10 } # 首次查询前等待
INTERVAL = 4 # 秒,建议 3–5
function wait_result(task_id, task_kind):
start = now()
sleep(INITIAL_WAIT[task_kind])
while now() - start < DEADLINE[task_kind]:
resp = http_post(GET_RESULT_URL,
body = { clientKey: read_from_env(), taskId: task_id },
timeout = 10)
if resp is network_error or resp.http_status == 429 or resp.http_status >= 500:
log(task_id, resp)
sleep(backoff()) # 网络/限流异常:退避后再查
continue
if resp.error_flag != 0: # 任务已失败:立即退出,不要继续等
return Failed(task_id, resp.error_code)
if resp.status == 'ready':
return Ready(task_id, resp.solution, elapsed = now() - start)
sleep(INTERVAL) # 仍在 processing:正常等待
return TimedOut(task_id, elapsed = now() - start)这段逻辑有几个要点:
- 首次查询前先等一会儿。 Token 任务刚提交就查,只会多出无效请求。
- 三种情况分开处理。 任务错误立即退出;网络异常或限流退避后再查;只有真正的 processing 才按正常间隔等待。
- 返回值带上耗时和 taskId。 方便按类型统计 P50/P95,出问题时也能拿样本找服务商核对。
- 密钥从环境变量或密钥管理服务读取。 不要写进代码、日志和截图。
如果用回调(webhook)代替轮询,本地仍然需要一个计时器,因为回调一直没到并不会自动变成失败信号。两种方式怎么选,见 验证码API轮询和回调有什么区别。
超时之后怎么处理
- 先判断原因,再决定是否重提。 不要拿同样的参数立刻无限重提。如果是参数或代理的问题,重提只会重复失败。
- Token 类任务要考虑页面状态。 浏览器页面停留太久,会话或页面上的挑战可能已经失效。重试前最好刷新页面,重新提取参数。
- 给重试设上限。 比如最多重试一到两次,超过就记录并跳过,避免单个任务拖住整个批次。
- 拿到结果不等于业务成功。 Token 返回后,目标站仍可能拒绝,这属于另一类问题,见 验证码 token 返回后验证失败怎么排查。连接超时、等待超时等不同超时的区分,可以看 验证码API超时怎么处理。
- 确认计费口径。 弄清你所用的平台对超时任务、失败任务分别怎么计费,再决定重试策略能承受多少次额外提交。
在 NexCaptcha 上排查时
NexCaptcha 采用 createTask / getTaskResult 的任务模式,支持轮询和 webhook。API 域名是 api.nexcaptcha.net,密钥放在请求体的 clientKey 字段中。上面的排查顺序同样适用:
- 确认 clientKey 和 taskId 对应同一账户、同一任务,并且代码处理了错误字段;
- 核对任务类型和页面参数;
- 检查代理链路。
官网说明按千次成功识别计费、失败退款。超时任务具体怎么计入、错误标识字段的确切名称,以官方文档和工程师的答复为准。
如果反复核对后,同一类任务仍然持续卡住,可以整理好任务类型、出问题的时间段、几个 taskId 样本以及是否使用代理,通过 NexCaptcha 官网 的工程师联系入口提交,方便对方定位问题。