Async hCaptcha tasks with Python and httpx
Use the native async Python client to wait for hCaptcha jobs without blocking an event loop, with explicit polling and client cleanup.
Use a native async transport
A synchronous HTTP call inside an async worker prevents that worker from making progress while the network is busy. The easycaptcha httpx backend provides AsyncHttpxClient, so creation, retrieval, and balance calls can be awaited. Install it with python -m pip install 'easycaptcha-sdk[httpx]'.
The urllib and requests async variants use threads around blocking HTTP. That can be appropriate when those libraries are required, but it is a different execution model. Choose deliberately rather than assuming every class with an async name uses native async networking.
Bound the wait without repeating creation
import asyncio
import os
import time
from easycaptcha.backends.httpx import AsyncHttpxClient
async def solve():
async with AsyncHttpxClient(os.environ["EASYCAPTCHA_API_KEY"]) as client:
job = await client.create_job({
"type": "hcaptcha_token",
"sitekey": os.environ["HCAPTCHA_SITEKEY"],
"url": os.environ["HCAPTCHA_PAGE_URL"],
"hold": 30,
})
task_id = job["task_id"]
deadline = time.monotonic() + 180
while job["status"] in ("queued", "running"):
if time.monotonic() >= deadline:
raise TimeoutError(f"Resume lookup for {task_id}")
await asyncio.sleep(1)
job = await client.get_job(task_id)
if job["status"] == "failed":
raise RuntimeError(job["error"]["detail"])
return job["result"]["token"]
# Call await solve() inside your application's event loop.
This function submits exactly once. Its deadline controls local waiting, not server-side cancellation. Persist the task ID outside the function if a background worker must recover after a restart.
Control admission separately from transport
Async networking makes it easy to start many operations; it does not increase your account’s weighted concurrency allowance. Put a queue or semaphore around task creation and release capacity when you observe terminal work. Do not hold a global database lock while waiting for a remote job.
Close the client through its async context manager. When using a supplied httpx session, its lifecycle belongs to your application. Test cancellation, transport failure, and a job that remains queued beyond your local deadline. The API reference supplies the job states; measurement guidance explains why a local timeout should remain an unknown outcome until reconciled.