{"id":26365,"date":"2026-10-08T06:37:06","date_gmt":"2026-10-08T06:37:06","guid":{"rendered":"https:\/\/capskip.com\/?p=26365"},"modified":"2026-10-08T06:37:06","modified_gmt":"2026-10-08T06:37:06","slug":"apscheduler-captcha-job","status":"publish","type":"post","link":"https:\/\/capskip.com\/zh\/apscheduler-captcha-job\/","title":{"rendered":"\u5982\u4f55\u5728 APScheduler \u4e2d\u8bc6\u522b\u9a8c\u8bc1\u7801\uff0c\u4e0d\u518d\u8df3\u8fc7\u8fd0\u884c"},"content":{"rendered":"<p>An APScheduler CAPTCHA job is an ordinary function that calls CapSkip, scheduled with an interval trigger. The solve itself is one line. The defaults around it are the problem. APScheduler runs only one instance of a job at a time. The synchronous client of the CapSkip Python SDK sets no timeout on its HTTP calls. Together, these mean that one solve that never returns blocks every later run, and the only sign is a warning every ten minutes. A run that starts more than one second late is dropped. And on Windows with Python 3.13 or older, Ctrl+C does nothing until the next run is due. This guide fixes all three problems in a job that logs in to a site behind reCAPTCHA every ten minutes.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">What you need<\/h2>\n<ul>\n<li>CapSkip running on a Windows machine, with the reCAPTCHA solver switched on.<\/li>\n<li>Python 3.10 or later. The code below was run on Python 3.13.7 and 3.14.0.<\/li>\n<li>APScheduler 3.11, the current stable line, plus the capskip and requests packages. Pin APScheduler below 4: the 4.0 pre-releases have a different API, and the imports below fail on them.<\/li>\n<li>An address for the solver. In Local mode CapSkip answers on 127.0.0.1 for that device only. In Server mode it listens on your network address or public IP, so a scheduler on another machine can call it over the API. Both are set under <a href=\"https:\/\/capskip.com\/setup-guide\/#connection-settings\">connection settings<\/a>.<\/li>\n<\/ul>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 1: a job that solves the CAPTCHA<\/h2>\n<p>An APScheduler job is any function. This one asks CapSkip for a reCAPTCHA token every ten minutes. Write 127.0.0.1 rather than localhost: on Windows, localhost tries the IPv6 address first, and in our runs that doubled the time to report a solver that is not running, from 2 seconds to 4.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"python\" class=\"EnlighterJSRAW\"># pip install &quot;apscheduler&lt;4&quot; capskip\nfrom apscheduler.schedulers.blocking import BlockingScheduler\nfrom capskip import CapSkip\n\nsolver = CapSkip(host=&quot;127.0.0.1&quot;, port=8080)\n\n\ndef solve_login_captcha():\n    result = solver.recaptcha(sitekey=&quot;YOUR_SITEKEY&quot;, url=&quot;https:\/\/example.com\/login&quot;)\n    print(&quot;token:&quot;, result[&quot;code&quot;][:20], &quot;...&quot;)\n\n\nscheduler = BlockingScheduler()\nscheduler.add_job(solve_login_captcha, &quot;interval&quot;, minutes=10)\nscheduler.start()<\/pre>\n<\/div>\n<p>BlockingScheduler runs in the main thread and sends each run to a pool of 10 worker threads. The first run happens one interval after start, not at start. That is fine until a solve misbehaves, which is what the rest of this guide is about. The short examples use BlockingScheduler to stay short. On Windows with Python 3.13 or older, use the loop from the full script below instead.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Why a slow solve skips the next run<\/h2>\n<p>Every job has max_instances set to 1 by default. When a run is due and the previous one is still running, APScheduler does not start a second one. It logs a warning instead, at WARNING level under the apscheduler.scheduler logger:<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"bash\" class=\"EnlighterJSRAW\">Execution of job &quot;solve_login_captcha (trigger: interval[0:10:00], next run at: 2026-10-07 13:10:00 +08)&quot; skipped: maximum number of running instances reached (1)<\/pre>\n<\/div>\n<p>For a CAPTCHA job that default is right. You never want two solves for the same login at once. A solve that takes a little longer than the interval costs one skipped run, and the job continues at the next scheduled time. In a test with a 5 second interval and a 12.8 second solve, two runs were skipped per solve, and the next run started at the following scheduled time, not immediately after the solve.<\/p>\n<p>The trouble starts when a solve never finishes. Then the one allowed instance stays busy forever, and every later run is skipped with the same warning. The job looks scheduled, the log looks alive, and nothing happens.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 2: give the SDK an HTTP timeout<\/h2>\n<p>The CapSkip Python SDK calls requests.post and requests.get without a timeout. If CapSkip accepts a connection and never answers, for example because the app froze, the call waits forever. The SDK&#8217;s recaptchaTimeout, 300 seconds by default, does not help, because it is only checked between polls, and a hung poll never ends. In our test, a job stuck this way stayed stuck after the solver recovered, logging only skip warnings.<\/p>\n<p>Two obvious fixes do not work. socket.setdefaulttimeout has no effect, because requests always passes its own timeout value down, even when that value is None. And the SDK takes no session object you could configure. What works is giving the SDK&#8217;s module a small stand-in for the requests package whose post and get calls always pass a timeout.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"python\" class=\"EnlighterJSRAW\"># pip install capskip requests\nimport functools\nimport types\n\nimport requests\nimport capskip.api\nfrom capskip import CapSkip\n\n\n# The SDK calls requests.post\/get with no timeout. Hand its module a\n# stand-in whose calls always pass one: 5 s to connect, 30 s to answer.\ncapskip.api.requests = types.SimpleNamespace(\n    RequestException=requests.RequestException,\n    post=functools.partial(requests.post, timeout=(5, 30)),\n    get=functools.partial(requests.get, timeout=(5, 30)),\n)  # do this before the first solve\nsolver = CapSkip(host=&quot;127.0.0.1&quot;, port=8080)<\/pre>\n<\/div>\n<p>This replaces the requests name inside capskip.api, the module that sends the SDK&#8217;s requests to CapSkip in capskip 1.3.0. Check it again after you upgrade the SDK. Run it once, before the first solve, and every call gets 5 seconds to connect and 30 seconds to answer.<\/p>\n<p>The two kinds of hang then end differently. If CapSkip stops answering while the task is being submitted, the job fails after 30 seconds with a NetworkException that says read timed out. If it stops answering while the SDK is polling for the result, the SDK treats the timeout as not ready yet and keeps polling, so the job ends at recaptchaTimeout with a TimeoutException. With the defaults, that took 333 seconds in our test. The worst case is therefore about five and a half minutes, which fits inside a ten minute interval, and the next run solves normally.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 3: choose what happens to late runs<\/h2>\n<p>The second default is misfire_grace_time, which is 1 second. A run that starts more than a second after its scheduled time is dropped, and APScheduler logs a warning that the run time was missed, with the delay. Runs start late more often than you would think. A pool whose threads are all busy solving, or a laptop that slept through a run, is enough.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"python\" class=\"EnlighterJSRAW\"># pip install &quot;apscheduler&lt;4&quot; capskip\nfrom datetime import datetime\n\nfrom apscheduler.schedulers.blocking import BlockingScheduler\nfrom capskip import CapSkip\n\nsolver = CapSkip(host=&quot;127.0.0.1&quot;, port=8080)\n\n\ndef solve_login_captcha():\n    result = solver.recaptcha(sitekey=&quot;YOUR_SITEKEY&quot;, url=&quot;https:\/\/example.com\/login&quot;)\n    print(&quot;token:&quot;, result[&quot;code&quot;][:20])\n\n\nscheduler = BlockingScheduler()\nscheduler.add_job(\n    solve_login_captcha, &quot;interval&quot;, minutes=10,\n    next_run_time=datetime.now(),  # first run now, not ten minutes from now\n    jitter=30,                     # add 0 to 30 s to each run\n    max_instances=1,               # the default: a slow solve skips the next run\n    coalesce=True,                 # the default: after a stall, run once\n    misfire_grace_time=None,       # default is 1 s; None runs late instead of dropping\n)\nscheduler.start()<\/pre>\n<\/div>\n<p>Set misfire_grace_time to None and a late run still happens, late. Leave the coalesce option at its default of True, so that after a long stall several missed runs collapse into one instead of running one after another. We paused a scheduler for 14 seconds on a 4 second interval to simulate a computer in sleep mode. With the defaults, the catch-up run was dropped. With misfire_grace_time set to None it ran once. With the coalesce option set to False as well, it ran three times in a row, three CAPTCHA solves for one login.<\/p>\n<p>Pass next_run_time to run the job right away at start. The jitter option adds a random delay of up to that many seconds to each run, which spreads requests out. Each delay is added to the previous run time, which already includes a delay, so the average period becomes the interval plus half the jitter.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 4: see failures as they happen<\/h2>\n<p>An exception raised in a job does not stop the scheduler. APScheduler logs it at ERROR level, with a message that the job raised an exception and the full traceback, and the job keeps its schedule. A listener gets the same events as objects, so you can count them, alert on them or write them where you want.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"python\" class=\"EnlighterJSRAW\"># pip install &quot;apscheduler&lt;4&quot; capskip\nimport logging\n\nfrom apscheduler.events import EVENT_JOB_ERROR, EVENT_JOB_MAX_INSTANCES, EVENT_JOB_MISSED\nfrom apscheduler.schedulers.blocking import BlockingScheduler\nfrom capskip import CapSkip\n\nlogging.basicConfig(level=logging.INFO)\nsolver = CapSkip(host=&quot;127.0.0.1&quot;, port=8080)\n\n\ndef solve_login_captcha():\n    solver.recaptcha(sitekey=&quot;YOUR_SITEKEY&quot;, url=&quot;https:\/\/example.com\/login&quot;)\n\n\ndef on_event(event):\n    if event.code == EVENT_JOB_ERROR:\n        logging.error(&quot;job %s failed: %r&quot;, event.job_id, event.exception)\n    elif event.code == EVENT_JOB_MAX_INSTANCES:\n        logging.warning(&quot;job %s still solving, this run skipped&quot;, event.job_id)\n    else:\n        logging.warning(&quot;job %s started too late, this run dropped&quot;, event.job_id)\n\n\nscheduler = BlockingScheduler()\nscheduler.add_listener(on_event, EVENT_JOB_ERROR | EVENT_JOB_MAX_INSTANCES | EVENT_JOB_MISSED)\nscheduler.add_job(solve_login_captcha, &quot;interval&quot;, minutes=10, id=&quot;login&quot;)\nscheduler.start()<\/pre>\n<\/div>\n<p>Three events cover a CAPTCHA job. EVENT_JOB_ERROR carries the exception itself, such as an ApiException whose message is ERROR_CAPTCHA_UNSOLVABLE. EVENT_JOB_MAX_INSTANCES fires once for each skipped run, which makes a stuck solve visible as a rising count. EVENT_JOB_MISSED fires for each dropped late run.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">The full APScheduler CAPTCHA job<\/h2>\n<p>This script fetches the login page with a requests session, reads the sitekey and the CSRF field, solves the reCAPTCHA with CapSkip and posts the form with the token. It catches CapSkip&#8217;s errors inside the job, so a failed solve is one warning and the next run tries again.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"python\" class=\"EnlighterJSRAW\"># pip install &quot;apscheduler&lt;4&quot; capskip requests\nimport functools\nimport logging\nimport re\nimport time\nimport types\nfrom datetime import datetime\n\nimport requests\nimport capskip.api\nfrom apscheduler.schedulers.background import BackgroundScheduler\nfrom capskip import CapSkip, CapSkipError\n\nLOGIN_URL = &quot;https:\/\/example.com\/login&quot;\nUSERNAME, PASSWORD = &quot;YOUR_USERNAME&quot;, &quot;YOUR_PASSWORD&quot;\n\nlogging.basicConfig(level=logging.INFO, format=&quot;%(asctime)s %(levelname)s %(name)s: %(message)s&quot;)\nlog = logging.getLogger(&quot;login-job&quot;)\n\n\n# The SDK's HTTP calls have no timeout; give them one so a stuck CapSkip\n# cannot hang the job.\ncapskip.api.requests = types.SimpleNamespace(\n    RequestException=requests.RequestException,\n    post=functools.partial(requests.post, timeout=(5, 30)),\n    get=functools.partial(requests.get, timeout=(5, 30)),\n)\nsolver = CapSkip(host=&quot;127.0.0.1&quot;, port=8080)\n\n\ndef field(html, pattern):\n    match = re.search(pattern, html)\n    if not match:\n        raise ValueError(f&quot;not found on the login page: {pattern}&quot;)\n    return match.group(1)\n\n\ndef login():\n    with requests.Session() as session:\n        page = session.get(LOGIN_URL, timeout=30)\n        sitekey = field(page.text, r'data-sitekey=&quot;([^&quot;]+)&quot;')\n        # The form's hidden CSRF field (Django names it csrfmiddlewaretoken).\n        csrf = field(page.text, r'name=&quot;csrfmiddlewaretoken&quot; value=&quot;([^&quot;]+)&quot;')\n        try:\n            token = solver.recaptcha(sitekey=sitekey, url=LOGIN_URL)[&quot;code&quot;]\n        except CapSkipError as exc:\n            log.warning(&quot;CapSkip could not solve: %s: %s&quot;, type(exc).__name__, exc)\n            return\n        reply = session.post(LOGIN_URL, timeout=30, headers={&quot;Referer&quot;: LOGIN_URL}, data={\n            &quot;csrfmiddlewaretoken&quot;: csrf,\n            &quot;username&quot;: USERNAME,\n            &quot;password&quot;: PASSWORD,\n            &quot;g-recaptcha-response&quot;: token,\n        })\n        # Many sites answer a failed login with 200 and the form again;\n        # check for something only a logged-in page shows.\n        if reply.ok:\n            log.info(&quot;logged in (HTTP %s)&quot;, reply.status_code)\n        else:\n            log.warning(&quot;login refused (HTTP %s): %s&quot;, reply.status_code, reply.text[:200])\n\n\nscheduler = BackgroundScheduler()\nscheduler.add_job(\n    login, &quot;interval&quot;, minutes=10,\n    next_run_time=datetime.now(),  # first run now, not ten minutes from now\n    max_instances=1,               # never two solves for the same account at once\n    coalesce=True,                 # after a stall, run once, not once per missed slot\n    misfire_grace_time=None,       # a late run still runs instead of being dropped\n)\nscheduler.start()\ntry:\n    while True:\n        time.sleep(1)  # Ctrl+C lands here at once, on any Python version\nexcept KeyboardInterrupt:\n    log.info(&quot;stopping; a solve in flight finishes first&quot;)\n    scheduler.shutdown()<\/pre>\n<\/div>\n<p>It uses BackgroundScheduler and a sleep loop instead of BlockingScheduler, for a reason that only appears on Windows. With BlockingScheduler on Python 3.13 or older, the main thread waits on a lock that Ctrl+C cannot interrupt, so the KeyboardInterrupt only arrives at the next scheduled wake-up. With a ten minute interval, that can be ten minutes. Python 3.14 fixed this on Windows, but the loop works on every version: Ctrl+C interrupts time.sleep at once.<\/p>\n<p>Stopping still waits for a solve that is in flight. Before the process exits, Python waits for the pool&#8217;s worker threads to finish, so a running solve finishes first. That happens whether or not you call scheduler.shutdown(), and with the timeout from Step 2 the wait lasts at most about five and a half minutes. A second Ctrl+C does not shorten it.<\/p>\n<p>The session posts the form with a Referer header, because requests sends none of its own and frameworks such as Django refuse an HTTPS form post without one. The session also keeps the cookie that matches the CSRF field. Solve right before you post: a reCAPTCHA token expires two minutes after it is issued. Invisible widgets, Enterprise keys and the other variants are covered in the <a href=\"https:\/\/capskip.com\/recaptcha-v2-python\/\">guide to solving reCAPTCHA v2 in Python<\/a>.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Many accounts on one schedule<\/h2>\n<p>One job per account works well, with one shared CapSkip client, which is safe to use from several threads. Make the thread pool as large as the number of accounts.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"python\" class=\"EnlighterJSRAW\"># pip install &quot;apscheduler&lt;4&quot; capskip\nimport logging\nfrom datetime import datetime\n\nfrom apscheduler.executors.pool import ThreadPoolExecutor\nfrom apscheduler.schedulers.blocking import BlockingScheduler\nfrom capskip import CapSkip\n\nlogging.basicConfig(level=logging.INFO)\nsolver = CapSkip(host=&quot;127.0.0.1&quot;, port=8080)  # one client is safe to share across threads\nACCOUNTS = [f&quot;user{i:02d}&quot; for i in range(20)]\n\n\ndef login(account):\n    token = solver.recaptcha(sitekey=&quot;YOUR_SITEKEY&quot;, url=&quot;https:\/\/example.com\/login&quot;)[&quot;code&quot;]\n    logging.info(&quot;%s got a token %s&quot;, account, token[:12])\n\n\nscheduler = BlockingScheduler(executors={&quot;default&quot;: ThreadPoolExecutor(max_workers=20)})\nfor account in ACCOUNTS:\n    scheduler.add_job(login, &quot;interval&quot;, minutes=10, args=[account], id=account,\n                      next_run_time=datetime.now(), misfire_grace_time=None)\nscheduler.start()<\/pre>\n<\/div>\n<p>The default pool has 10 threads. With 20 jobs due at once, 10 runs waited for a thread, started 7.8 seconds late and were dropped under the default 1 second grace time. Half the accounts never logged in, and only the missed warnings said so. A pool of 20 threads, or misfire_grace_time set to None, fixes that.<\/p>\n<p>CapSkip has a limit of its own. Its reCAPTCHA settings allow 10 solves at a time by default, under Max. Threads, and later tasks wait for a free thread. In our test with 20 threads, 10 solves took 7.8 seconds and the other 10 took 12.8, because they waited 5 seconds for CapSkip. Raise Max. Threads in CapSkip if your machine has the capacity, and spread the accounts with jitter if it does not.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">With asyncio instead of threads<\/h2>\n<p>If the rest of your code is async, use AsyncIOScheduler with AsyncCapSkip, the SDK&#8217;s native async client. Coroutine jobs run on the event loop, and max_instances works exactly as before.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"python\" class=\"EnlighterJSRAW\"># pip install &quot;apscheduler&lt;4&quot; capskip\nimport asyncio\n\nfrom apscheduler.schedulers.asyncio import AsyncIOScheduler\nfrom capskip import AsyncCapSkip\n\nsolver = AsyncCapSkip(host=&quot;127.0.0.1&quot;, port=8080)\n\n\nasync def solve_login_captcha():\n    result = await solver.recaptcha(sitekey=&quot;YOUR_SITEKEY&quot;, url=&quot;https:\/\/example.com\/login&quot;)\n    print(&quot;token:&quot;, result[&quot;code&quot;][:20], &quot;...&quot;)\n\n\nasync def main():\n    scheduler = AsyncIOScheduler()\n    scheduler.add_job(solve_login_captcha, &quot;interval&quot;, minutes=10)\n    scheduler.start()\n    await asyncio.Event().wait()  # keep the event loop running\n\n\nasyncio.run(main())<\/pre>\n<\/div>\n<p>Two things differ. AsyncCapSkip is built on httpx, which has a 5 second default timeout, so a stuck CapSkip ends in an exception without the patch from Step 2: after 5 seconds if the submit hangs, and at recaptchaTimeout if a poll hangs. A NetworkException from it can have an empty message, so log the exception type as well. And stopping cancels a solve in flight instead of waiting for it: the job then logs a CancelledError as its exception. The traceback also shows a NetworkException above it, which is only the SDK&#8217;s not ready signal, not a network failure. Ctrl+C exits in a fraction of a second.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Running CapSkip on another machine<\/h2>\n<p>The scheduler and the solver do not have to share a PC. Switch CapSkip to Server mode under connection settings, and it listens on your network address or public IP. Then point the client there.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"python\" class=\"EnlighterJSRAW\"># Replace the solver line with this when CapSkip runs in Server mode\n# on another Windows machine on your network.\nsolver = CapSkip(apiKey=&quot;YOUR_API_KEY&quot;, host=&quot;192.168.1.50&quot;, port=8080)<\/pre>\n<\/div>\n<p>Use a static public IP if the route crosses the internet, and turn on API key validation, with the same key in the client. CapSkip opens its port to every address when Server mode starts, so to limit access to the machines you expect, add a Windows Firewall block rule for the others. The solver is still your own Windows machine, and solves are still unmetered.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Common APScheduler CAPTCHA errors<\/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>Skipped: maximum number of running instances reached (1), now and then<\/td>\n<td>A solve took longer than the interval<\/td>\n<td>Nothing, or a longer interval<\/td>\n<\/tr>\n<tr>\n<td>The same skipped warning on every run, and no solves<\/td>\n<td>One solve is stuck on an HTTP call with no timeout<\/td>\n<td>Give the SDK a timeout, as in Step 2<\/td>\n<\/tr>\n<tr>\n<td>Run time of job was missed by some seconds<\/td>\n<td>The run started more than 1 second late and was dropped<\/td>\n<td>Set misfire_grace_time to None<\/td>\n<\/tr>\n<tr>\n<td>Half the accounts never run<\/td>\n<td>More jobs were due than the 10 threads of the default pool<\/td>\n<td>Size the pool to the number of jobs<\/td>\n<\/tr>\n<tr>\n<td>Ctrl+C does nothing for minutes<\/td>\n<td>BlockingScheduler on Windows with Python 3.13 or older<\/td>\n<td>Use BackgroundScheduler with a sleep loop, or Python 3.14<\/td>\n<\/tr>\n<tr>\n<td>The job raised an exception, ApiException ERROR_CAPTCHA_UNSOLVABLE<\/td>\n<td>CapSkip could not solve this task<\/td>\n<td>Catch CapSkipError in the job; the next run tries again<\/td>\n<\/tr>\n<tr>\n<td>NetworkException: Read timed out. (read timeout=30)<\/td>\n<td>CapSkip accepted the task and stopped answering<\/td>\n<td>Check that CapSkip responds; the next run tries again<\/td>\n<\/tr>\n<tr>\n<td>TimeoutException: timeout 300.0 exceeded<\/td>\n<td>CapSkip stopped answering, or was closed, while the SDK was polling<\/td>\n<td>Check that CapSkip is running; the next run tries again<\/td>\n<\/tr>\n<tr>\n<td>NetworkException: the target machine actively refused it<\/td>\n<td>CapSkip is not running, or the host or port is wrong<\/td>\n<td>Start CapSkip and check the address against its connection settings<\/td>\n<\/tr>\n<tr>\n<td>ModuleNotFoundError: No module named apscheduler.schedulers<\/td>\n<td>APScheduler 4 is installed, for example a pre-release<\/td>\n<td>Install APScheduler below version 4<\/td>\n<\/tr>\n<tr>\n<td>The site rejects the token<\/td>\n<td>The token expired, or the post left out a cookie, a hidden field or the Referer<\/td>\n<td>Solve right before posting, in the session that fetched the page<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\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;\">Can I store these jobs in a database?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">Yes, with SQLAlchemyJobStore, but mind what gets saved. The job&#8217;s function is stored by reference, so it must live in an importable module: a lambda fails with a ValueError, and a function from the script you ran as __main__ is removed when another program loads the store. Pass the CapSkip client through a module variable rather than the job&#8217;s args. A client in args is pickled into the database with its host, port and API key, and the saved job keeps that address until you add the job again with the same id and replace_existing set to True. Give the job an id in any case: without one, every start of the program adds another copy.<\/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;\">Why not set a timeout on the recaptcha call itself?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">Because the SDK does not take one there. Passing the timeout argument to solver.recaptcha raises a TypeError in capskip 1.3.0, and the recaptchaTimeout setting on the client limits only the polling loop and is checked only between requests. Neither one limits a single HTTP call, which is why Step 2 patches the requests calls themselves.<\/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;\">When is a task queue a better fit than APScheduler?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">When other code creates the work, or when each piece of work must be kept and retried if the process crashes. APScheduler runs jobs inside your own program on a timer. A queue such as RQ stores each job in Redis and runs it in separate workers, with its own job timeout to plan around, as explained in the <a href=\"https:\/\/capskip.com\/rq-captcha-job\/\">guide to solving CAPTCHAs in RQ jobs<\/a>.<\/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 scheduler run on a different machine from CapSkip?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">Yes. Put CapSkip in Server mode under connection settings, so it listens on a network address instead of loopback, and pass that address as the host of the client. The scheduler then calls it over the same HTTP API from any machine that can reach the port. Add a Windows Firewall block rule for the addresses that should not reach it. The solver still runs on hardware you own, and no solve goes through a third-party service.<\/p>\n<\/details>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">The short version<\/h2>\n<p>An APScheduler CAPTCHA job needs more than the solve. Give the SDK&#8217;s HTTP calls a timeout, because with max_instances at 1 a single stuck solve skips every later run. Set misfire_grace_time to None and keep coalesce on, so late runs happen once instead of being dropped. Size the thread pool to your jobs, catch CapSkipError inside the job, and use BackgroundScheduler with a sleep loop if you stop it with Ctrl+C on Windows with Python 3.13 or older. The <a href=\"https:\/\/apscheduler.readthedocs.io\/en\/3.x\/userguide.html\" rel=\"nofollow noopener\" target=\"_blank\">APScheduler 3.x user guide<\/a> documents every option used here.<\/p>\n<ul>\n<li>Every CAPTCHA type the Python package handles: <a href=\"https:\/\/capskip.com\/python-captcha-solver\/\">the Python CAPTCHA solver page<\/a>.<\/li>\n<li>How the checkbox reCAPTCHA works and what the solver covers: <a href=\"https:\/\/capskip.com\/recaptcha-v2-solver\/\">the reCAPTCHA v2 solver page<\/a>.<\/li>\n<\/ul>\n<p>A job that runs every ten minutes makes over four thousand solves a month. Running them through a <a href=\"https:\/\/capskip.com\/\">captcha solver<\/a> on your own Windows machine means that number never turns into a bill.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>\u5728 APScheduler \u7684\u9ed8\u8ba4\u8bbe\u7f6e\u4e0b\uff0c\u4e00\u6b21\u5361\u4f4f\u7684\u9a8c\u8bc1\u7801\u8bc6\u522b\u4f1a\u8ba9\u4e4b\u540e\u7684\u6bcf\u4e00\u6b21\u8fd0\u884c\u90fd\u88ab\u8df3\u8fc7\uff0c\u800c\u5f00\u59cb\u65f6\u95f4\u665a\u4e86\u4e00\u79d2\u4ee5\u4e0a\u7684\u8fd0\u884c\u4f1a\u88ab\u4e22\u5f03\u3002\u672c\u6587\u7ed9\u51fa\u4e00\u4e2a interval \u4f5c\u4e1a\uff1a\u5b83\u7528 CapSkip \u8bc6\u522b reCAPTCHA\uff0c\u7ed9 SDK \u8bbe\u7f6e\u8d85\u65f6\uff0c\u5e76\u4fdd\u6301\u539f\u6709\u7684\u8c03\u5ea6\u3002<\/p>","protected":false},"author":1,"featured_media":26364,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"APScheduler CAPTCHA Jobs That Never Stall (Python) | CapSkip","rank_math_description":"An apscheduler captcha job in Python: solve reCAPTCHA with CapSkip on an interval, stop a stuck solve from skipping every run, and keep late runs.","rank_math_focus_keyword":"apscheduler captcha","footnotes":""},"categories":[70],"tags":[],"class_list":["post-26365","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\/26365","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=26365"}],"version-history":[{"count":1,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/posts\/26365\/revisions"}],"predecessor-version":[{"id":26366,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/posts\/26365\/revisions\/26366"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/media\/26364"}],"wp:attachment":[{"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/media?parent=26365"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/categories?post=26365"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/tags?post=26365"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}