{"record":{"id":"4eb23fb6c302c883","repo":"unclecode/crawl4ai","slug":"too-many-concurrent-jobs-for-this-caller","errorCode":null,"errorMessage":"Too many concurrent jobs for this caller","messagePattern":"Too many concurrent jobs for this caller","errorType":"http","errorClass":"HTTPException","httpStatus":429,"severity":"warning","filePath":"deploy/docker/api.py","lineNumber":48,"sourceCode":"from llm_broker import LLMProviderNotAllowed\nfrom crawl4ai.utils import perform_completion_with_backoff\n\n\ndef _enqueue_job(background_tasks, factory, principal=None):\n    \"\"\"Submit a background job to the bounded work queue (per-principal quota).\n\n    Falls back to FastAPI BackgroundTasks when the queue isn't running (tests /\n    no lifespan). Maps queue/quota limits to HTTP 503 / 429.\n    \"\"\"\n    from work_queue import get_job_queue, QueueFull, QuotaExceeded\n    q = get_job_queue()\n    if q is None or not q.started:\n        background_tasks.add_task(factory)\n        return\n    try:\n        q.submit(factory, principal)\n    except QuotaExceeded:\n        raise HTTPException(status_code=429, detail=\"Too many concurrent jobs for this caller\")\n    except QueueFull:\n        raise HTTPException(\n            status_code=503, detail=\"Server busy, retry later\",\n            headers={\"Retry-After\": \"5\"},\n        )\n\n\ndef _attach_declarative_hooks(crawler, hooks_config: dict) -> dict:\n    \"\"\"Build and attach server-authored hooks from declarative specs.\n\n    Raises HookValidationError on an unknown action / invalid params, which the\n    handlers map to HTTP 400.\n    \"\"\"\n    specs = hooks_config.get(\"hooks\", []) or []\n    hooks = build_declarative_hooks(specs)\n    for hook_point, fn in hooks.items():\n        crawler.crawler_strategy.set_hook(hook_point, fn)\n    return {\"status\": \"success\", \"attached\": list(hooks.keys())}","sourceCodeStart":30,"sourceCodeEnd":66,"githubUrl":"https://github.com/unclecode/crawl4ai/blob/7e801521428ee12509994d39151006f64055ebe3/deploy/docker/api.py#L30-L66","documentation":"HTTP 429 raised by the Docker crawl4ai server's queue dispatcher (deploy/docker/api.py:48) when work_queue.QuotaExceeded is thrown — the submitting caller (principal) already holds the maximum number of concurrent in-flight jobs allowed by its per-caller quota. It is a rate/quota signal, not a server failure: the request is rejected so the caller should slow down or await completion of running jobs.","triggerScenarios":"A single API client firing more concurrent /md, /llm, or crawl requests than its configured quota (e.g. quota=2 and the client sends a 3rd before any finishes); a retry storm from a misconfigured client effectively multiplying concurrency; shared service account used by several workers whose combined load exceeds one principal's quota.","commonSituations":"Load-testing the server with one token; batch scripts with ThreadPoolExecutor sized above the quota; multiple developers/pods sharing the same credential (same principal sub) and hitting the aggregate cap.","solutions":["Client-side: cap concurrency at or below the quota (semaphore / smaller thread pool) — most common fix.","Honor 429 with exponential backoff and honor any Retry-After guidance, resubmitting when a slot frees.","If legitimate, raise the per-caller quota in the server's work_queue configuration.","Use distinct principals if multiple independent clients share one identity."],"exampleFix":"# before\nimport asyncio\nawait asyncio.gather(*[client.post(\"/md\", json=p) for p in payloads])  # 429s\n\n# after\nsem = asyncio.Semaphore(2)  # match server per-caller quota\nasync def one(p):\n    async with sem:\n        for attempt in range(5):\n            r = await client.post(\"/md\", json=p)\n            if r.status_code != 429:\n                return r\n            await asyncio.sleep(2 ** attempt)","handlingStrategy":"retry","validationCode":"async def quota_slots_free(client, principal_concurrency: int, quota: int) -> bool:\n    return principal_concurrency < quota  # track in-flight requests per token locally","typeGuard":null,"tryCatchPattern":"resp = await client.post(\"/md\", json=body)\nif resp.status_code == 429:\n    await asyncio.sleep(float(resp.headers.get(\"Retry-After\", 1)) or 1)\n    resp = await client.post(\"/md\", json=body)  # bounded retry loop","preventionTips":["Cap client concurrency below the per-caller quota with a semaphore","Use one principal per independent client, not one shared token","Retry with backoff on 429; never tight-loop"],"tags":["http-429","rate-limit","quota","server","crawl4ai"],"backgroundTag":null,"analyzedSha":"7e801521428ee12509994d39151006f64055ebe3","analyzedAt":"2026-08-14T20:46:20.673Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}