两种方式拿到的是同一个识别结果,区别在于由谁通知谁。
- 轮询:你的程序提交任务后,每隔几秒问服务端一次“好了没有”。
- 回调:你在提交任务时留下一个公网地址,服务端算完后把结果主动 POST 过来。
下面几类环境用轮询最省事:本地脚本、单机运行的 Playwright/Selenium、没有公网入口的内网环境。
下面几类场景用回调更合适:分布式采集、由消息队列驱动的后端、并发很高的任务池。回调能减少空请求,结果到达也更及时。代价是你要自己维护一个可靠的接收端,而且最好保留轮询作为兜底。
两种方式的流程
验证码识别 API 大多采用异步任务模型。你先调用 createTask 提交任务,拿到一个 taskId,识别过程在服务端进行。reCAPTCHA、Cloudflare Turnstile、GeeTest 这类返回 token 的任务,通常要几秒到几十秒才能出结果。轮询和回调的差别,只在于结果怎么回到你手里。

轮询(拉取)
- 调用 createTask,拿到 taskId。
- 按固定间隔或逐步拉长的间隔调用 getTaskResult。
- 状态还是“处理中”就继续等;变成“完成”时取出 token 或坐标;返回错误时进入异常处理。
回调(推送)
- 在 createTask 请求中带上回调地址。有的平台还要求先在后台登记接收域名,例如 2Captcha 的 pingback 机制。
- 任务完成后,平台向这个地址发起 HTTP 请求,把结果推送过来。
- 你的接收端解析结果,再交给后续业务处理。
五个关键差异
| 维度 | 轮询 | 回调 |
|---|---|---|
| 网络要求 | 只需能访问外网 | 需要公网可达的 HTTPS 接收端 |
| 结果感知延迟 | 额外受轮询间隔影响 | 完成后即推送 |
| 请求次数 | 处理中会有多次空查询 | 完成时一次推送 |
| 代码结构 | 在同一流程里线性等待 | 提交和接收分离,需要关联 ID |
| 失败风险 | 主要是超时和限流 | 推送丢失、接收端宕机或响应慢 |
1. 网络条件决定了能不能用回调
轮询只需要出站访问。程序在 NAT 后面、办公电脑上、没有固定公网 IP 的机器上都能运行。
回调则要求你对外暴露一个可路由、稳定在线的接收地址,通常还要配置 HTTPS 证书、防火墙放行和入站防护。如果你的自动化跑在笔记本或内网 CI 里,单为回调去搭一个公网入口并不划算。
2. 延迟:轮询间隔会直接加到等待时间上
回调在任务完成时就发出通知。轮询要等到下一次查询才能发现结果已经好了。
粗略估算:假设轮询间隔为 T,结果完成的时刻在两次查询之间均匀分布,那么平均多等约 T/2,最坏多等约 T。以 5 秒间隔为例,平均多等约 2.5 秒,最坏接近 5 秒。这是按假设推出的估算,不是实测值。
对单个任务来说,这点差距往往不明显。但如果链路里有多次验证码,或者 token 有效期较短(reCAPTCHA 等 token 的有效期一般以分钟计),这段额外等待就会挤占后续操作的时间。
3. 请求量:轮询的空查询会累积
一个任务处理 20 秒、每 3 秒查一次,就会产生六七次没有结果的请求。并发上千个任务时,这些空查询会明显增加你的出站连接数,也会给服务端带来压力。查询过密还可能触发服务商的限流。
回调把多次查询压缩成一次推送。量越大,这一点越有价值。
4. 代码结构:回调需要自己做结果对应
用轮询时,Playwright 或 Puppeteer 脚本可以在同一个函数里提交任务、等待结果,然后立刻把 token 注入页面。浏览器会话、页面状态和结果天然在一起。
用回调时,提交任务和接收结果发生在两个不同的服务里。你需要用 taskId(或自己的关联 ID)把推送回来的结果对应到正确的任务上,比如:
- 唤醒挂起的浏览器会话;
- 更新数据库里的那条记录;
- 把结果投递给下游队列。
对应错了,就会出现“token 注入到另一个页面”这类很难排查的问题。
5. 失败处理:回调不是一次就能保证送达
推送可能因为网络抖动、接收端宕机或响应太慢而失败。部分平台对接收端的响应时限要求很严,例如 CapMonster Cloud 文档要求接收端在 2 秒内响应。
轮询的失败面相对简单:主要是总超时和限流,处理逻辑都在你自己的进程里。
怎么选
适合轮询的情况
- 本地或单机脚本。
- 用浏览器自动化做自有系统、或已获授权系统的测试。
- 并发不高的场景。
- 没有公网入口的环境。
CI 里的回归测试也通常用轮询。另外,如果你测的是自有站点,先看验证码厂商是否提供官方测试密钥,能用测试密钥就不必调用识别服务,可参考 Playwright 验证码测试怎么做。
适合回调的情况
- 无状态后端或多 worker 的分布式采集。
- 由队列驱动的流水线。
- 并发高、对结果感知延迟敏感的业务。
生产环境的稳妥做法:回调为主,轮询兜底
平时靠回调拿结果。如果某个任务超过预期时长仍未收到推送,就主动调用 getTaskResult 查一次状态,避免任务永久挂起。这次查询也能顺便用来对账。
轮询的通用写法
以下为通用伪代码。字段名和状态值以所用服务的文档为准。
task_id = createTask(任务参数)
sleep(初始等待) # token 类任务很少在一两秒内完成
deadline = now() + 总超时
while now() < deadline:
r = getTaskResult(task_id)
if r 表示完成: return r 中的结果
if r 表示错误: 按错误类型重试或放弃
sleep(间隔) # 常见 3~5 秒,可逐步拉长
记录 task_id,按超时处理有三点要注意:
- 间隔不要过密:间隔太短,空查询和限流风险都会上升。
- 必须设置总超时:否则任务异常时,循环会一直跑下去。
- 记录 taskId:出问题时,你可以拿着它和服务商核对。
完整的 Python 接入示例可参考 Python 验证码识别 API 接入。
回调接收端要做的几件事
以下同样是通用伪代码。具体的请求格式和校验方式以服务商文档为准。
POST /captcha-callback
校验来源(签名、共享密钥参数或来源 IP,取决于服务商)
解析 taskId 和结果
若该 taskId 已处理过:直接返回 200(保证幂等)
写入存储或投递到队列
立即返回 200,不在这里执行耗时业务为什么要幂等:平台在重试推送时,同一条结果可能到达不止一次。
为什么要立刻返回:把注入 token、提交表单这类慢操作放进接收端,很容易超过平台的响应时限,平台会认为推送失败。
最后还要记住,接收端收到结果,只说明识别结果已经返回。目标站点是否接受这个结果、业务是否真正完成,需要在下游另外判断和统计。
在 NexCaptcha 上怎么用
NexCaptcha 官网介绍的是统一的任务模式:
- 接口:createTask、getTaskResult、getBalance;
- 接口域名:api.nexcaptcha.net;
- 密钥:放在请求体的 clientKey 字段;
- 取结果方式:轮询和 webhook 两种。
用轮询时,按上面的循环写即可。打算用回调时,下面几项要在接入前通过官方文档或工程师确认,再据此编写接收端:
- 推送请求体的格式;
- 是否带签名,以及如何校验;
- 推送失败后的重试次数和间隔;
- 对接收端的响应时限要求。
这些信息和支持的验证码类型,可在 NexCaptcha 官网 查看或联系工程师确认。