{"id":25637,"date":"2026-09-11T15:11:01","date_gmt":"2026-09-11T15:11:01","guid":{"rendered":"https:\/\/capskip.com\/?p=25637"},"modified":"2026-09-24T10:54:04","modified_gmt":"2026-09-24T10:54:04","slug":"celery-captcha-task","status":"publish","type":"post","link":"https:\/\/capskip.com\/zh\/celery-captcha-task\/","title":{"rendered":"\u5982\u4f55\u5728 Celery \u4efb\u52a1\u4e2d\u8bc6\u522b\u9a8c\u8bc1\u7801\uff08Python \u961f\u5217\uff09"},"content":{"rendered":"<p>A Celery captcha task has one rule that shapes everything else: it must solve from scratch on every attempt. Celery gives you at-least-once delivery, so a task can run twice, and a CapSkip result can be read once. Store a captcha id and resume from it after a retry and you get nothing back. Solve, use the token and finish, all inside one task body.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">What you need<\/h2>\n<ul>\n<li>Celery 5 with a broker, Redis or RabbitMQ. The broker choice changes one setting later on.<\/li>\n<li>CapSkip running on a Windows machine, with the Python client installed in the worker image.<\/li>\n<li>Server mode, in almost every real deployment. Workers usually run in Linux containers, and the solver does not.<\/li>\n<li>The sitekey and page URL, passed in as task arguments rather than baked into the task.<\/li>\n<\/ul>\n<div data-no-translation>\n<pre data-enlighter-language=\"bash\" class=\"EnlighterJSRAW\"># pip install capskip\r\npip install -U celery[redis] capskip<\/pre>\n<\/div>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 1: the task<\/h2>\n<p>The whole solve is one call. The SDK submits, polls and returns the token, so there is no id to carry between steps and nothing to persist.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"python\" class=\"EnlighterJSRAW\"># pip install capskip\r\nimport os\r\nfrom celery import Celery\r\nfrom capskip import CapSkip, NetworkException\r\n\r\napp = Celery(&quot;solves&quot;, broker=&quot;redis:\/\/redis:6379\/0&quot;)\r\n\r\n# CAPSKIP_HOST is the solver machine. Loopback only works if the\r\n# worker runs on the same Windows box as CapSkip.\r\nsolver = CapSkip(\r\n    host=os.environ.get(&quot;CAPSKIP_HOST&quot;, &quot;127.0.0.1&quot;),\r\n    port=int(os.environ.get(&quot;CAPSKIP_PORT&quot;, &quot;8080&quot;)),\r\n    apiKey=os.environ.get(&quot;CAPSKIP_API_KEY&quot;, &quot;capskip&quot;),\r\n)\r\n\r\n@app.task(\r\n    bind=True,\r\n    autoretry_for=(NetworkException,),\r\n    retry_backoff=True,\r\n    max_retries=3,\r\n    soft_time_limit=330,\r\n    time_limit=360,\r\n)\r\ndef solve_and_submit(self, sitekey, page_url):\r\n    result = solver.recaptcha(sitekey=sitekey, url=page_url)\r\n    return submit_form(page_url, result[&quot;code&quot;])   # token, used here<\/pre>\n<\/div>\n<p>Note what is not in there. No captcha id in the return value, no second task to submit the token, no result stored for later. The token is used in the same task that made it. The reason is timing rather than tidiness, and it is covered under retries below.<\/p>\n<p>That call is reCAPTCHA v2. The other types CapSkip supports are the same shape: pass invisible or enterprise set to 1, or version set to v3 with an action, or call turnstile or geetest instead. The full surface is on <a href=\"https:\/\/capskip.com\/python-captcha-solver\/\">the Python CAPTCHA solver page<\/a>.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 2: two time limits, and where to put them<\/h2>\n<p>Celery has no time limit by default, on either setting. That is the wrong default for a task that waits on a network service, because a stuck solve occupies a worker slot forever.<\/p>\n<p>Set both, and set them above what the SDK itself allows. CapSkip gives a reCAPTCHA, Turnstile or GeeTest solve three hundred seconds and an image CAPTCHA one hundred and twenty, both configurable on the client. If the soft limit fires first, Celery raises SoftTimeLimitExceeded inside your task and you lose the SDK&#8217;s own TimeoutException, which is the more useful signal because it tells you the solver was reached and did not finish.<\/p>\n<table>\n<thead>\n<tr>\n<th>Which setting<\/th>\n<th>Suggested value for a solve<\/th>\n<th>Why<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>soft_time_limit<\/td>\n<td>330 seconds<\/td>\n<td>Thirty seconds above the solver&#8217;s own ceiling, so the SDK reports first<\/td>\n<\/tr>\n<tr>\n<td>time_limit<\/td>\n<td>360 seconds<\/td>\n<td>The backstop. The worker kills the process at this point<\/td>\n<\/tr>\n<tr>\n<td>recaptchaTimeout on the client<\/td>\n<td>300 seconds, the default<\/td>\n<td>Lower it if you would rather fail fast than wait<\/td>\n<\/tr>\n<tr>\n<td>defaultTimeout on the client<\/td>\n<td>120 seconds, the default<\/td>\n<td>Image CAPTCHAs only. They rarely take seconds, let alone minutes<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>If you run image CAPTCHAs and reCAPTCHA through the same worker, give them separate tasks with separate limits rather than one task with the higher pair. A three hundred second ceiling on a job that normally finishes in a second hides real failures for five minutes.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 3: the retry rule that is specific to solving<\/h2>\n<p>Celery&#8217;s automatic retries are exactly right for a solver that is restarting and exactly wrong for a token you already hold. The difference is worth being precise about.<\/p>\n<p>With retry_backoff set to True the first retry waits one second, then two, then four, then eight, and jitter is on by default so the real delay is a random value up to that maximum. The cap is retry_backoff_max, which defaults to six hundred seconds. Compare that with a reCAPTCHA token, which is good for about two minutes.<\/p>\n<p>So a task that solved successfully, stored the token, failed on the submit and then retried can be resumed after a delay several times longer than the token&#8217;s life. It will fail on a token that was perfectly valid when it was made, and the log will blame the target site. That failure mode is covered in the guide to <a href=\"https:\/\/capskip.com\/recaptcha-token-expiration\/\">reCAPTCHA token expiration<\/a>.<\/p>\n<p>The fix is the task shape above: solve inside the retry, not before it.<\/p>\n<p>Restrict autoretry_for to the exceptions that describe a transport problem. NetworkException means CapSkip was not reachable, which is worth retrying. ApiException and ValidationException mean the request was wrong and will be wrong again. TimeoutException is a judgement call, and usually worth one retry rather than three.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 4: acks_late, and why a result can only be read once<\/h2>\n<p>By default Celery acknowledges a message just before running it, so a worker that dies mid-task loses the job. Turning on task_acks_late moves the acknowledgement to after the task finishes, so a crashed worker&#8217;s job is redelivered and runs again. That is usually what you want for work that costs money elsewhere, and it is what makes the read-once rule matter.<\/p>\n<p>A CapSkip result is readable once. If the first attempt submitted the challenge, read the token and then crashed before acknowledging, the redelivered attempt cannot re-read that id. It has to submit again. The task above does exactly that, because it holds no state between attempts, and the cost of resolving is a few seconds of your own hardware rather than a second charge.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"python\" class=\"EnlighterJSRAW\"># Redelivery is safe here because the task resolves rather\r\n# than resuming. Pair it with reject_on_worker_lost so a\r\n# killed worker requeues instead of dropping the job.\r\napp.conf.task_acks_late = True\r\napp.conf.task_reject_on_worker_lost = True\r\n\r\n# Long tasks and a prefetch of 4 means idle workers sit on\r\n# queued jobs. Drop it to 1 for solve queues.\r\napp.conf.worker_prefetch_multiplier = 1<\/pre>\n<\/div>\n<p>That last one is the quiet performance bug in most solve queues. The default prefetch multiplier is four, so each worker process reserves four messages up front. With tasks that take milliseconds that is a win. With tasks that wait on a solve, three of those four sit reserved behind a job that is doing nothing but polling, while another worker has nothing to do.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 5: the broker setting that duplicates solves<\/h2>\n<p>If your broker is Redis, there is one more number. Redis has no native acknowledgement, so Celery emulates it with a visibility timeout: the number of seconds it waits for a worker to acknowledge a task before redelivering the message to another worker. It defaults to one hour, and it lives inside broker_transport_options rather than being a setting of its own.<\/p>\n<p>One hour is comfortably above a three hundred second solve, so the default is safe. The trouble starts when someone lowers it to make failed jobs recover faster, because Celery&#8217;s own documentation warns that a task whose execution time exceeds the visibility timeout gets executed again, and again, in a loop. Every slow solve then runs at least twice.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"python\" class=\"EnlighterJSRAW\"># Keep this above your hard time limit, not near it.\r\n# 3600 is the default and it is fine. If you must lower it,\r\n# stay well clear of the 360 second time_limit above.\r\napp.conf.broker_transport_options = {&quot;visibility_timeout&quot;: 3600}<\/pre>\n<\/div>\n<p>RabbitMQ acknowledges natively and has no equivalent setting, which is one reason to prefer it for queues full of slow tasks.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 6: running the solver somewhere the workers can reach<\/h2>\n<p>Celery workers usually run in Linux containers on a cluster. CapSkip runs on Windows. So in practice the worker and the solver are on different machines, and loopback is not the answer.<\/p>\n<p>CapSkip has two connection modes for this. Local binds to 127.0.0.1 and serves that device only. Server binds to your network address or public IP, so another box, a container host or a hosted platform can reach the same Windows machine over the API. Both live under <a href=\"https:\/\/capskip.com\/setup-guide\/#connection-settings\">connection settings<\/a>, and Server mode changes only which address the solver listens on. It is still your hardware and it is still unmetered.<\/p>\n<p>That last point is what makes a queue firing constantly reasonable to run at all.<\/p>\n<table>\n<thead>\n<tr>\n<th>Where the workers run<\/th>\n<th>Which connection mode<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>On the same Windows machine as CapSkip<\/td>\n<td>Local mode, host stays 127.0.0.1<\/td>\n<\/tr>\n<tr>\n<td>In Docker or on another box on your network<\/td>\n<td>Server mode with the solver&#8217;s LAN address<\/td>\n<\/tr>\n<tr>\n<td>On a managed platform or a cloud cluster<\/td>\n<td>Server mode with a static public IP and a firewall rule<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Read the host and key from the environment rather than the code. The Python client does not read CAPSKIP_HOST, CAPSKIP_PORT or CAPSKIP_API_KEY by itself, which is why the task in Step 1 reads them and passes them in. A worker container then needs those variables and nothing else.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Solving many at once<\/h2>\n<p>Two ways, and they suit different shapes of work. One task per CAPTCHA, with worker concurrency doing the parallelism, is the normal answer and it is what the prefetch note above is about. For a batch that arrives together, Python&#8217;s client has a genuine async implementation, so one task can gather a batch inside itself.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"python\" class=\"EnlighterJSRAW\">import asyncio\r\nimport os\r\nfrom capskip import AsyncCapSkip\r\n\r\n# AsyncCapSkip in Python is a real async client, not an alias.\r\nasync def solve_batch(pairs):\r\n    solver = AsyncCapSkip(\r\n        host=os.environ.get(&quot;CAPSKIP_HOST&quot;, &quot;127.0.0.1&quot;), port=8080)\r\n    return await asyncio.gather(*[\r\n        solver.recaptcha(sitekey=k, url=u) for k, u in pairs\r\n    ])\r\n\r\n@app.task(soft_time_limit=330, time_limit=360)\r\ndef solve_many(pairs):\r\n    return [r[&quot;code&quot;] for r in asyncio.run(solve_batch(pairs))]<\/pre>\n<\/div>\n<p>Worth knowing before you copy that into another language: the Node and .NET clients name a class AsyncCapSkip too, but there it is an alias rather than a second implementation. Python is the one where it means something. The full comparison is in the guide to <a href=\"https:\/\/capskip.com\/solve-captchas-parallel-python\/\">solving CAPTCHAs in parallel in Python<\/a>.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Common errors and what they mean<\/h2>\n<table>\n<thead>\n<tr>\n<th>What you see<\/th>\n<th>Cause<\/th>\n<th>Fix<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>NetworkException on every task<\/td>\n<td>CapSkip is bound to loopback and the worker is elsewhere<\/td>\n<td>Switch to Server mode and set CAPSKIP_HOST to the solver&#8217;s address<\/td>\n<\/tr>\n<tr>\n<td>SoftTimeLimitExceeded instead of TimeoutException<\/td>\n<td>The soft limit is below the client&#8217;s own ceiling<\/td>\n<td>Raise soft_time_limit above 300, or lower recaptchaTimeout<\/td>\n<\/tr>\n<tr>\n<td>The same job runs twice on a slow solve<\/td>\n<td>The Redis visibility timeout is shorter than the task<\/td>\n<td>Raise it well above the hard time limit<\/td>\n<\/tr>\n<tr>\n<td>A retry fails with an expired token<\/td>\n<td>The token was solved before the retry, not inside it<\/td>\n<td>Move the solve inside the task body, as above<\/td>\n<\/tr>\n<tr>\n<td>Reading the same captcha id returns nothing<\/td>\n<td>A CapSkip result is readable once<\/td>\n<td>Never persist an id across attempts. Resolve instead<\/td>\n<\/tr>\n<tr>\n<td>ERROR_WRONG_USER_KEY in an ApiException<\/td>\n<td>CAPSKIP_API_KEY is unset in the worker environment<\/td>\n<td>Set it in the worker environment and restart the worker<\/td>\n<\/tr>\n<tr>\n<td>Idle workers while a queue backs up<\/td>\n<td>Prefetch is reserving long tasks behind long tasks<\/td>\n<td>Set worker_prefetch_multiplier to 1 on solve queues<\/td>\n<\/tr>\n<tr>\n<td>Jobs vanish when a worker is killed<\/td>\n<td>Late acknowledgement is off<\/td>\n<td>Turn on task_acks_late and task_reject_on_worker_lost<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>The key errors are worth reading about on their own, because the same response covers a key that is missing and a key that is simply wrong: <a href=\"https:\/\/capskip.com\/error-wrong-user-key\/\">how to fix ERROR_WRONG_USER_KEY<\/a>.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">FAQ<\/h2>\n<details style=\"border:1px solid #e2e5ee;border-radius:10px;padding:14px 18px;margin:0 0 12px;\">\n<summary style=\"cursor:pointer;\">\n<h3 style=\"font-size:1.15rem;line-height:1.4;display:inline;margin:0;\">Should the solve and the submit be two separate tasks?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">No. It is a tempting split, because the two halves fail for different reasons and a chain looks tidier in the monitor. But a reCAPTCHA token lasts about two minutes and a queued task can sit for longer than that, so the second half regularly runs against a token that has already expired. Keep them together and let the whole thing retry as a unit. Resolving costs you a few seconds of your own machine.<\/p>\n<\/details>\n<details style=\"border:1px solid #e2e5ee;border-radius:10px;padding:14px 18px;margin:0 0 12px;\">\n<summary style=\"cursor:pointer;\">\n<h3 style=\"font-size:1.15rem;line-height:1.4;display:inline;margin:0;\">Is acks_late safe with a CAPTCHA solve?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">Yes, as long as the task resolves rather than resumes. Late acknowledgement means a crashed worker&#8217;s job is redelivered and runs a second time, so the task has to be safe to repeat. A task that submits a fresh challenge each time is. A task that stored an id and tries to re-read it is not, because a result can be read once. The version in this guide is the safe shape.<\/p>\n<\/details>\n<details style=\"border:1px solid #e2e5ee;border-radius:10px;padding:14px 18px;margin:0 0 12px;\">\n<summary style=\"cursor:pointer;\">\n<h3 style=\"font-size:1.15rem;line-height:1.4;display:inline;margin:0;\">Can the workers run on Linux if the solver runs on Windows?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">Yes, and that is the normal arrangement. The worker only needs to reach an HTTP endpoint, so it can be a Linux container anywhere on the network while the solver runs on a Windows machine in Server mode. Point CAPSKIP_HOST at that machine. Nothing about the solve becomes metered or remote in the sense that matters: the hardware is still yours.<\/p>\n<\/details>\n<details style=\"border:1px solid #e2e5ee;border-radius:10px;padding:14px 18px;margin:0 0 12px;\">\n<summary style=\"cursor:pointer;\">\n<h3 style=\"font-size:1.15rem;line-height:1.4;display:inline;margin:0;\">How is this different from running solves in Airflow?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">Airflow schedules a graph of steps and passes data between them, so the interesting question there is which boundary the token crosses. Celery is a queue, so the interesting question is what happens when the same message is delivered twice. The solving call is identical. The boundary question is worked through in full in <a href=\"https:\/\/capskip.com\/airflow-captcha-dag\/\">the Airflow DAG guide<\/a>.<\/p>\n<\/details>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">The short version<\/h2>\n<p>Put the solve and whatever uses the token in one task. Set a soft limit of 330 and a hard limit of 360 so the client&#8217;s own timeout reports first. Retry only on NetworkException, and let the retry resolve rather than resume, because a backoff can outlive a token by a long way and a result can be read once. Turn on late acknowledgement, drop the prefetch multiplier to 1, and keep the Redis visibility timeout far above the hard limit. Run CapSkip in Server mode whenever the workers are not on the solver&#8217;s own machine.<\/p>\n<ul>\n<li>The reCAPTCHA v2 checkbox itself is covered on <a href=\"https:\/\/capskip.com\/recaptcha-v2-solver\/\">the reCAPTCHA v2 solver page<\/a>.<\/li>\n<li>The raw endpoints behind the client are documented in <a href=\"https:\/\/capskip.com\/api-docs\/\">the CapSkip API documentation<\/a>.<\/li>\n<\/ul>\n<p>One last thing to weigh before you size the queue: CapSkip is a <a href=\"https:\/\/capskip.com\/\">captcha solver<\/a> that runs on hardware you already own, so a hundred workers hammering it and one worker trickling through the same jobs cost the same nothing.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>CapSkip \u7684\u7ed3\u679c\u53ea\u80fd\u8bfb\u53d6\u4e00\u6b21\uff0c\u6240\u4ee5 Celery \u91cd\u8bd5\u65f6\u5982\u679c\u60f3\u62ff\u5b58\u4e0b\u6765\u7684\u9a8c\u8bc1\u7801 id \u63a5\u7740\u7528\uff0c\u4ec0\u4e48\u4e5f\u62ff\u4e0d\u5230\u3002\u4e0b\u9762\u662f\u80fd\u625b\u4f4f worker \u5d29\u6e83\u7684\u4efb\u52a1\u5199\u6cd5\u3001\u9700\u8981\u8bbe\u7f6e\u7684\u4e24\u4e2a\u65f6\u95f4\u4e0a\u9650\uff0c\u4ee5\u53ca\u90a3\u4e2a\u4f1a\u6084\u6084\u8ba9\u8bc6\u522b\u6b21\u6570\u7ffb\u500d\u7684 broker \u9009\u9879\u3002<\/p>","protected":false},"author":1,"featured_media":25636,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"Celery CAPTCHA Task: Retries Done Right | CapSkip","rank_math_description":"A Celery captcha task must resolve on every retry, never resume. Here are the time limits, the acks_late trap, and the broker option that doubles work.","rank_math_focus_keyword":"celery captcha","footnotes":""},"categories":[70],"tags":[],"class_list":["post-25637","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-captcha"],"_links":{"self":[{"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/posts\/25637","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/comments?post=25637"}],"version-history":[{"count":4,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/posts\/25637\/revisions"}],"predecessor-version":[{"id":26062,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/posts\/25637\/revisions\/26062"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/media\/25636"}],"wp:attachment":[{"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/media?parent=25637"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/categories?post=25637"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/tags?post=25637"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}