How to Solve CAPTCHAs in Huey Tasks With SQLite on Windows

To solve CAPTCHAs in Huey, keep the queue in a SqliteHuey file next to your tasks module, call CapSkip inside a task that has retries, and start the consumer with thread workers. On Windows that is the whole stack: one SQLite file, no Redis server, and the solver on the same PC. This huey captcha setup has four traps, and this guide covers each one. The consumer is an exe, not huey_consumer.py. Huey’s timeout option does nothing on thread workers, so one stuck solve holds a worker forever. With the defaults, reading a result reports the first failed attempt while Huey is still retrying. And CancelExecution takes retry as its first argument, so the obvious way to stop retrying can retry forever instead.
What you need
- CapSkip running on a Windows machine, with the reCAPTCHA solver switched on.
- Python 3.10 or later, the floor of the CapSkip SDK. Huey 3.4.0 declares no minimum of its own. Everything below was run on Python 3.13.7 and 3.14.0 with huey 3.4.0 and capskip 1.3.0.
- No Redis. SqliteHuey keeps the queue, the schedule, locks and results in one SQLite file.
- 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 consumer on another machine can call it over the API. Both are set under connection settings.
Step 1: install Huey and the CapSkip SDK
Create a virtual environment in your project folder and install three packages. The requests package is used by the full example to fetch and post the login page, and by the timeout fix later on.
# Windows, in PowerShell or cmd: a virtual environment in your project folder python -m venv .venv .venv\Scripts\python -m pip install huey capskip requests
Step 2: create the queue and the solver client
Everything Huey needs lives in tasks.py. The consumer imports it to find the tasks, and every script that queues work imports it too.
# tasks.py
import os
from pathlib import Path
from capskip import CapSkip
from huey import SqliteHuey
# The whole queue is one SQLite file. Keep it next to this module, so the
# consumer and every script that imports tasks use the same file, whatever
# folder they are started from.
huey = SqliteHuey(
"captcha",
filename=str(Path(__file__).with_name("huey.db")),
store_intermediate_errors=False, # get() waits for the last retry
)
# Local mode: CapSkip on this PC. Server mode: set CAPSKIP_HOST (and
# CAPSKIP_API_KEY if key validation is on) to reach it on another machine.
solver = CapSkip(
apiKey=os.environ.get("CAPSKIP_API_KEY", "capskip"),
host=os.environ.get("CAPSKIP_HOST", "127.0.0.1"),
port=int(os.environ.get("CAPSKIP_PORT", "8080")),
)Name the database file with a full path, as above. The default filename is huey.db in whatever folder the process was started from. A script started from another folder then creates its own empty huey.db, queues its task there, and waits for a result that never comes. Pinning the file next to tasks.py gives every process the same queue. While the consumer runs, two helper files, huey.db-wal and huey.db-shm, sit beside it. They belong to SQLite’s write-ahead log, are normal, and can stay after a hard stop.
SQLite handled the load without errors. Twelve workers and three scripts queuing 600 tasks between them got no error saying the database is locked. Inside each process Huey shares one connection behind a lock, and between processes SQLite waits up to 5 seconds for the file. One thing to know about the file: task arguments and results are pickled into it as plain bytes. A password passed as an argument was readable in the table, so keep secrets in the environment or in tasks.py, not in task arguments.
Step 3: write the Huey CAPTCHA task, and choose what to retry
Every exception a Huey task raises uses up one retry. That is right for some CapSkip failures and wasteful for others, so the task sorts them.
from capskip import ApiException, ValidationException
from huey import CancelExecution
# Any exception uses one of the two retries, 15 s apart: NetworkException
# when CapSkip is not running, TimeoutException, an unsolvable task.
@huey.task(retries=2, retry_delay=15)
def solve_recaptcha(sitekey, page_url):
try:
return solver.recaptcha(sitekey=sitekey, url=page_url)["code"]
except (ApiException, ValidationException) as exc:
if "ERROR_CAPTCHA_UNSOLVABLE" in str(exc):
raise # this attempt failed; a fresh one may not
# ERROR_GOOGLEKEY, ERROR_PAGEURL, ERROR_BAD_PARAMETERS and
# ERROR_KEY_DOES_NOT_EXIST fail the same way on every attempt.
# CancelExecution's first argument is retry: False ends the task now.
raise CancelExecution(False, f"{type(exc).__name__}: {exc}") from excThree failures are worth another attempt: a NetworkException when CapSkip is not running yet, a TimeoutException, and ERROR_CAPTCHA_UNSOLVABLE, which is how CapSkip reports a solve that did not work this time. The consumer logs the error with its traceback, then a line saying it is requeueing the task with the number of retries left. With a retry_delay of 15 seconds, the next attempt started 16.1 seconds after the failure in our runs.
A malformed sitekey, a page URL without http or https, or a wrong API key (with key validation on) is refused before any solve starts, with ERROR_GOOGLEKEY, ERROR_PAGEURL or ERROR_KEY_DOES_NOT_EXIST. An empty sitekey or page URL gets ERROR_BAD_PARAMETERS. These fail the same way every time, so retrying only wastes time. Huey’s CancelExecution ends a task without a retry only when retry is False. Raised bare, it still retries while the task has retries left. Read its signature too: its first positional argument is retry, not the message. Written as CancelExecution with just a message, the message becomes the retry value and counts as true. The log then says the task will be retried, and the task runs again and again until you stop the consumer. Passing the message together with retry=False raises a TypeError, because retry would then get two values. Pass False first, as the code does. The consumer then logs a WARNING that the task raised CancelExecution and will not be retried.
One limit of this sorting: a sitekey that Google itself refuses still reaches you as ERROR_CAPTCHA_UNSOLVABLE, because CapSkip reports every failed solve that way. That case costs three attempts before it fails. If a page fails every time, check its sitekey first.
Step 4: start the consumer on Windows
Run the consumer from the folder that holds tasks.py. No activation is needed when you call the exe by its path.
# In the folder that holds tasks.py, in PowerShell or cmd .venv\Scripts\huey_consumer tasks.huey -w 4
Older guides, and the consumer’s own usage hint, say huey_consumer.py. Huey 3 installs only huey_consumer.exe, so that name fails in PowerShell and in cmd with a message that it is not recognized. The command above works in both, and so does .venv\Scripts\python -m huey.bin.huey_consumer tasks.huey. Started from another folder, the consumer stops with an error importing tasks.huey and ModuleNotFoundError: No module named tasks, because it only adds the current folder to its search path.
Thread workers are the only real choice on Windows. With -k process the consumer exits at once with an error saying that huey cannot be run in process-mode on Windows, because process workers need fork(), which Windows does not have. Greenlet workers need gevent installed. Threads suit this job anyway, since a worker spends almost all of a solve waiting for CapSkip.
Each worker sends its own request, so the worker count is also the number of solves CapSkip receives at once. With 12 workers and 12 queued tasks, all 12 requests reached CapSkip within 30 milliseconds. CapSkip’s reCAPTCHA section runs 10 solves at a time by default, set by Max. Threads, and queues the rest. Four workers suit a few logins. Raise -w toward that limit when the queue is long.
Step 5: queue a solve and read the result once
Calling the task does not run it. It writes the task to the queue and returns a Result handle you can wait on.
# enqueue.py, next to tasks.py: .venv\Scripts\python enqueue.py
from huey.exceptions import ResultTimeout, TaskException
from tasks import huey, solve_recaptcha
result = solve_recaptcha("YOUR_SITEKEY", "https://example.com/login")
try:
# Three attempts of up to about 5.5 minutes each, plus two 15 s delays.
token = result.get(blocking=True, timeout=1100)
except TaskException as exc:
print("solve failed:", exc)
except ResultTimeout:
print("no result yet: is the consumer running?")
else:
print("token:", token[:30], "...")
# Reading the result deleted it from huey.db. This handle keeps its
# copy, but a new handle for the same task id finds nothing.
print("read again:", huey.result(result.id)) # NoneThe 1100 second wait covers the worst case: three attempts that each run to the SDK’s limit, plus two retry delays. A failed task raises TaskException, whose message is the last error and whose metadata holds the retries left and the traceback. If nothing answers in time, you get ResultTimeout, which usually means the consumer is not running or is using another database file.
The store_intermediate_errors line in Step 2 matters here. With Huey’s default, each failed attempt is stored as the result. In our test, get() raised TaskException after the first failure while Huey ran the retry. The retry’s result was visible only after calling reset() on the handle. Switched off, get() waits for the last attempt.
A result can be read once. Reading it deletes it from the file, as CapSkip does with its own answers. The handle that read it keeps its copy, but huey.result() with the same task id returns None afterwards. Pass preserve=True to get() if two parts of your code need the value. A task that returns None stores nothing at all, so waiting on it ends in ResultTimeout.
Why a stuck solve needs its own timeout
Huey 3 accepts a timeout option on a task, but on thread workers it does nothing. A thread cannot be interrupted from outside, so Huey only checks it if the task itself calls task.check_timeout(). A task with timeout=2 that slept for five seconds finished normally. Process workers, where a timeout could be enforced, are refused on Windows.
That matters because the CapSkip SDK sets no HTTP timeout on its requests. If CapSkip accepts a connection and never answers, the call waits forever. In our test, two workers sat in that state with no log line at all, and the queued tasks behind them never started. They were still stuck 45 seconds after the solver recovered. The fix is to give the SDK’s HTTP calls a timeout. Put this in tasks.py, at module level, so it runs before any solve.
import functools
import types
import requests
import capskip.api
# capskip 1.3.0 calls requests.post/get with no timeout, and Huey cannot stop
# a thread worker that is stuck in one. Give every SDK call 5 s to connect
# and 30 s to answer. Module level in tasks.py, so it runs before any solve.
capskip.api.requests = types.SimpleNamespace(
RequestException=requests.RequestException,
post=functools.partial(requests.post, timeout=(5, 30)),
get=functools.partial(requests.get, timeout=(5, 30)),
)With it, a hung submit raised NetworkException after 30.0 seconds, and the task was retried. A hung status check lasts longer, because the SDK treats a failed poll as not ready yet and keeps polling until its own recaptchaTimeout of 300 seconds. That attempt ended with TimeoutException after about 333 seconds, and the retry solved. Either way the worker is free again. The fix replaces the requests reference inside capskip.api, an internal part of the SDK, so check it again when you upgrade capskip.
A scheduled solve that cannot overlap itself
A periodic task can repeat a solve on a timer, for example to keep a login fresh. The full example below does the actual login. Periodic tasks take no arguments, so the sitekey and URL live in the function.
from huey import crontab
# Every 15 minutes. The lock goes under periodic_task: when a run is due
# while the previous one is still solving, another worker skips it instead
# of starting a second solve for the same login.
@huey.periodic_task(crontab(minute="*/15"))
@huey.lock_task("login")
def refresh_login():
result = solver.recaptcha(sitekey="YOUR_SITEKEY", url="https://example.com/login")
print("new token:", result["code"][:30], "...")The consumer checks its periodic tasks once when it starts, then every 60 seconds counted from that start. So a run due in the current minute starts at once. If a solve takes longer than the interval, Huey still queues the next run. With several workers, a free worker would start it while the first run is still solving. The lock stops that: the new run logs a WARNING that it was not run, unable to acquire the lock, and is skipped. With one worker nothing overlaps, but late runs pile up in the queue and then execute one after another. Add an expires argument in seconds, such as expires=600, to the periodic_task decorator to drop runs that waited too long.
Locks live in the database file, so they survive a crash. After a hard stop, a lock left behind makes every later run skip. Start the consumer with -f to clear decorator locks, and it logs that it found stale locks. Huey 3.4 also added a ttl option to lock_task, but SqliteHuey does not support it and raises NotImplementedError when the lock is taken.
The full Huey CAPTCHA task file
This version fetches the login page, reads the sitekey and the CSRF token, solves the CAPTCHA right before the post and submits the form with a Referer header. It offers the login on demand as log_in and on a schedule as scheduled_login. Both share one lock, so they never solve for the same account at the same time. Replace the URL and the credentials, then start the consumer with -f.
# tasks.py: pip install huey capskip requests
# Start: .venv\Scripts\huey_consumer tasks.huey -w 4 -f
import functools
import logging
import os
import re
import types
from pathlib import Path
import requests
import capskip.api
from capskip import ApiException, CapSkip, ValidationException
from huey import CancelExecution, SqliteHuey, crontab
LOGIN_URL = "https://example.com/login"
USERNAME, PASSWORD = "YOUR_USERNAME", "YOUR_PASSWORD"
huey = SqliteHuey(
"captcha",
filename=str(Path(__file__).with_name("huey.db")), # same file from any folder
store_intermediate_errors=False, # get() waits for the last retry
)
log = logging.getLogger("huey.captcha") # printed by the consumer
# The SDK's HTTP calls have no timeout; give them one so a stuck CapSkip
# cannot hold a worker thread forever.
capskip.api.requests = types.SimpleNamespace(
RequestException=requests.RequestException,
post=functools.partial(requests.post, timeout=(5, 30)),
get=functools.partial(requests.get, timeout=(5, 30)),
)
solver = CapSkip(
apiKey=os.environ.get("CAPSKIP_API_KEY", "capskip"),
host=os.environ.get("CAPSKIP_HOST", "127.0.0.1"),
port=int(os.environ.get("CAPSKIP_PORT", "8080")),
)
def solve(sitekey, page_url):
try:
return solver.recaptcha(sitekey=sitekey, url=page_url)["code"]
except (ApiException, ValidationException) as exc:
if "ERROR_CAPTCHA_UNSOLVABLE" in str(exc):
raise # worth another attempt
# A refused sitekey, page URL or API key fails the same way every time.
log.error("not retrying: %s: %s", type(exc).__name__, exc)
raise CancelExecution(False, f"{type(exc).__name__}: {exc}") from exc
def field(html, pattern):
match = re.search(pattern, html)
if not match:
raise ValueError(f"not found on the login page: {pattern}")
return match.group(1)
def login():
with requests.Session() as session:
page = session.get(LOGIN_URL, timeout=30)
sitekey = field(page.text, r'data-sitekey="([^"]+)"')
csrf = field(page.text, r'name="csrfmiddlewaretoken" value="([^"]+)"')
token = solve(sitekey, LOGIN_URL) # solve right before the post
reply = session.post(LOGIN_URL, timeout=30, headers={"Referer": LOGIN_URL}, data={
"csrfmiddlewaretoken": csrf,
"username": USERNAME,
"password": PASSWORD,
"g-recaptcha-response": token,
})
reply.raise_for_status() # a refused login fails the attempt
log.info("logged in (HTTP %s)", reply.status_code)
return reply.status_code
# Both tasks take the same lock, so an on-demand login never runs while a
# scheduled one is solving, and the other way round.
# On demand: from tasks import log_in; log_in().get(blocking=True, timeout=1100)
@huey.task(retries=2, retry_delay=15) # a locked attempt uses a retry
@huey.lock_task("login")
def log_in():
return login()
# On a schedule; a run that comes due while the lock is held is skipped.
@huey.periodic_task(crontab(minute="*/15"))
@huey.lock_task("login")
def scheduled_login():
login()The solve happens inside the same attempt as the post, so a retry always fetches a fresh page and solves again. A token is never carried over from a failed attempt, which matters because Google lets a reCAPTCHA token live for two minutes. A refused login that returns an HTTP error, such as a 403, fails the attempt with raise_for_status() and is retried twice with a new page and a new solve each time. CapSkip does not charge per solve, so that costs nothing but time. Many sites answer a wrong password with 200 and the form again, so add a check for your site, such as the URL after the post. An on-demand log_in that finds the scheduled run holding the lock is skipped with the same WARNING, and that attempt uses a retry. Two retries 15 seconds apart give it about 30 seconds of waiting in all. If the scheduled solve takes longer, all three attempts are skipped and get() raises TaskException, with TaskLockedException in its message. Raise retry_delay on log_in if your solves usually take longer than that.
To log in on demand from another script, import log_in from tasks, call it, and wait on the result with get(blocking=True, timeout=1100), as in Step 5. The return value is the final HTTP status after the post. Log lines from the task appear in the consumer’s window next to Huey’s own.
Stopping the consumer cleanly
Press Ctrl+C once in the consumer’s window. Huey stops taking new tasks, waits for the running ones and exits. In our runs a consumer stopped in the middle of a solve exited 18 seconds later, as soon as that solve finished. An idle one exited in under a tenth of a second. A second Ctrl+C behaves differently by version: Python 3.14 exits at once, while 3.13 still waits for the running task, so a stuck solve without the timeout above could only be killed.
Anything faster than a graceful stop loses the task that was running. That includes a second Ctrl+C on 3.14, Ctrl+Break, and the -t option, which caps the graceful wait: with -t 5 the consumer exited about 5.1 seconds after Ctrl+C. The lost task had already left the queue, so it does not run again, and its lock stays behind. That is why the full example starts with -f.
Running CapSkip on another machine
The consumer 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. Your tasks.py already reads the address from the environment, so set the variables before you start the consumer.
# PowerShell, before starting the consumer: CapSkip runs in Server mode # on another Windows machine on your network $env:CAPSKIP_HOST = "192.168.1.50" $env:CAPSKIP_API_KEY = "YOUR_API_KEY" # $env:CAPSKIP_PORT = "8080" only if you changed the port .venv\Scripts\huey_consumer tasks.huey -w 4 -f
Use a static public IP if the route crosses the internet, and turn on API key validation, with the same key in CAPSKIP_API_KEY. CapSkip opens its port to every address when Server mode starts, so to keep it to the machines you expect, add a Windows Firewall block rule for the others. The consumer can then run on any machine that reaches the port, and the solver is still your own Windows machine with unmetered solves.
Common Huey CAPTCHA errors
| What you see | Cause | Fix |
|---|---|---|
| huey_consumer.py is not recognized | Huey 3 installs huey_consumer.exe only | Run .venv\Scripts\huey_consumer |
| huey cannot be run in process-mode on Windows | Process workers need fork() | Use the default thread workers |
| ModuleNotFoundError: No module named tasks | The consumer started outside the folder that holds tasks.py | Start it from that folder |
| ResultTimeout although the consumer is running | The script and the consumer use different huey.db files | Give SqliteHuey a full path to the file |
| get() reports a failure, then the task succeeds | store_intermediate_errors is on, so each failed attempt is stored | Set store_intermediate_errors=False |
| A task meant to stop keeps retrying | CancelExecution got the message as its retry argument | Raise CancelExecution(False, message) |
| Workers stop picking up tasks and log nothing | A solve is stuck in an HTTP call with no timeout | Give the SDK’s requests a timeout |
| Every scheduled run is skipped | A stale lock from a consumer that was killed | Restart the consumer with -f |
FAQ
Can I use RedisHuey instead of SqliteHuey?
Yes. The tasks, retries and locks in this guide work the same way. Install the redis package as well, then create the queue with RedisHuey instead of SqliteHuey. RedisHuey needs a Redis server, though, and Windows does not ship one. On the PC that already runs CapSkip, the SQLite file saves you running a second service. For a queue spread over many machines, Redis is the better fit, as in this guide to solving CAPTCHAs in Celery tasks.
Why does the first task after an idle period start late?
An idle consumer checks the database less and less often, up to every 10 seconds by default. With four workers, the first task after an idle period waited 3 to 7 seconds in our runs. Start the consumer with -m 1 to cap that wait at one second; it then stayed under one second.
Does this work in a Django project?
Yes, through huey.contrib.djhuey. Set huey_class to huey.SqliteHuey and a filename in the HUEY setting, and start the consumer with manage.py run_huey. One default surprises people: with DEBUG on, djhuey runs tasks immediately inside the request, so a solve blocks the page until CapSkip answers. Set immediate to False in the HUEY setting to test the real queue.
Can the consumer run on a different machine from CapSkip?
Yes. Put CapSkip in Server mode under connection settings, so it listens on a network address instead of loopback, and set CAPSKIP_HOST to that address before the consumer starts. The tasks then call 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.
The short version
For a Huey CAPTCHA task on Windows, pin the SqliteHuey file next to tasks.py, switch off store_intermediate_errors, and retry only the CapSkip failures that a new attempt can fix, ending the rest with CancelExecution(False, message). Start .venv\Scripts\huey_consumer with thread workers from the folder that holds tasks.py, give the SDK’s HTTP calls a timeout, expect to read each result only once, and put a lock on scheduled solves. The Huey consumer documentation lists every consumer option used here.
- Every CAPTCHA type the Python package handles: the Python CAPTCHA solver page.
- How the checkbox reCAPTCHA works and what the solver covers: the reCAPTCHA v2 solver page.
- The solve call itself, with every option: the guide to solving reCAPTCHA v2 in Python.
A queue in one file and a solver on the same PC keep the whole job on hardware you control. That is the point of a local captcha bypass: every retry is free, so the task can afford to try again.
