Troubleshooting and FAQ
Start with the terminal WorkItemResult, its error_category, and
result.timing. Enable normal Python logging for async_batch_llm before
raising concurrency or retry budgets; more work often amplifies the original
problem.
Throughput stops below the worker count
Symptom. Increasing concurrency or max_workers no longer raises
throughput, and admission or HTTP pool waits grow.
Likely cause. The httpx/OpenAI client connection pool is smaller than the worker pool, an explicit provider-admission limit is lower, the provider is throttling, or the process is close to its file-descriptor limit. A configured startup ramp also intentionally begins below steady-state concurrency.
How to confirm. Read capacity warnings at processor startup. Compare
concurrency, max_workers, max_provider_concurrency, the strategy's
max_concurrency, and client max_connections. Inspect
result.admission_wait_seconds, provider timing, HTTP pool metrics, and
ulimit -n on Unix.
Fix. Prefer unified concurrency=N with built-in clients so aligned limits
resize together. If using an existing client, size its pool explicitly and
advertise truthful CallableStrategy(max_concurrency=N). Lower concurrency or
raise the OS soft descriptor limit deliberately; do not hide a small pool
behind more workers. Allow the startup ramp to reach steady state before
judging throughput.
Related guide. Choosing Your Limits and the Production Checklist.
Persistent 429 responses
Symptom. Many items repeatedly enter cooldown, total time grows, or adding workers makes the run slower.
Likely cause. The provider account's request/token quota is below offered load, a suggested wait is long, the rate-limit retry budget is too generous, or a gateway and ABL are both retrying the same transport failure.
How to confirm. Check provider quota dashboards and response headers. Look
for coordinated-cooldown logs, repeated logical attempt numbers, suggested
waits, and RateLimitConfig.max_rate_limit_retries exhaustion. Inspect gateway
logs for hidden retries beneath each ABL attempt.
Fix. Reduce concurrency or set a conservative
max_requests_per_minute; request higher provider quota when appropriate.
Honor coordinated cooldowns rather than starting more workers. Choose one
layer to own transport retries at full strength and keep the other layer's
policy minimal. Bound rate-limit retries so a permanently throttled endpoint
eventually produces a terminal result.
Related guide. Choosing Your Limits and Callable integration.
An item takes longer than attempt_timeout
Symptom. A logical item runs for several multiples of the configured attempt timeout.
Likely cause. attempt_timeout limits one call to strategy.execute().
Provider-capacity admission, HTTP-client waiting, coordinated cooldowns, retry
backoff, and later attempts are outside that single-attempt budget.
How to confirm. Compare each result.timing.attempts entry with total item
time and admission/cooldown/backoff fields. Distinguish the SDK/httpx timeout
from ABL's attempt timeout. On Python 3.10 ABL uses its compatibility timeout
path, but the semantics are the same.
Fix. Set the HTTP client timeout for network phases, attempt_timeout for
one provider attempt, GuardrailConfig.total_timeout_per_item for the whole
logical item, and GuardrailConfig.batch_timeout for the run. Size them in
that order; a batch deadline can stop admission while active work is drained
or cancelled according to abort_mode.
Related guide. Deadlines and Fail-Fast Guardrails.
Empty, blocked, or tool-only responses
Symptom. A provider returns HTTP success but there is no usable text, or an
item fails with EmptyResponseError.
Likely cause. Content filtering or a Gemini safety block, length termination, a response containing only tool calls, or an actually empty choice. Text-oriented built-in strategies do not silently treat tool-only responses as text.
How to confirm. Inspect result.exception, provider finish/stop reason,
typed provider metadata, safety ratings, tool-call views, and token usage. A
billed empty response can still contribute failed-attempt tokens.
Fix. Adjust the prompt or safety configuration where policy permits,
increase the output limit for length termination, or use a strategy/parser
that intentionally handles tool calls. Do not retry a policy block blindly.
Use TokenTrackingError in a custom callable when a billed response fails
application parsing so its usage is retained.
Related guide. Provider guides and Callable usage and metadata.
OpenRouter reports an error inside HTTP 200
Symptom. The HTTP request succeeds but the OpenRouter payload contains an upstream provider error.
Likely cause. OpenRouter can encode a provider failure in an otherwise successful HTTP response.
How to confirm. The built-in model raises ProviderResponseError; inspect
its embedded code and message rather than only the HTTP status. Embedded 429s
are classified as rate limits.
Fix. Use OpenRouterStrategy and its recommended classifier, or reproduce
the same payload check in a custom client and supply an appropriate
error_classifier. Keep retry ownership clear if OpenRouter or another
gateway also retries upstream failures.
Related guide. OpenRouter Integration.
A run appears to hang
Symptom. No new result is observed even though the process is alive.
Likely cause. The consumer may be slow, a bounded result queue may be
applying intended backpressure, a post-processor or callback may block, a
provider call may be long, or workers may be in cooldown/backoff. Artifact I/O
can also delay checkpoint-before-publication. At the low level, preloading a
bounded queue before workers start cannot make progress. A consumer that stops
draining process_stream() leaves publishers waiting until generator cleanup.
An external gateway may be retrying invisibly underneath ABL.
How to confirm. Check in this order:
- Is the stream consumer still awaiting the next result?
- Are
max_result_queue_sizeslots full while the sink writes slowly? - Is a post-processor or custom callback doing blocking I/O?
- Do attempt/provider timings show one long call?
- Do logs show cooldown or retry backoff?
- Is artifact storage slow or unavailable?
- Was a low-level bounded queue filled before
start()/process_all()? - Do gateway logs show nested retries?
At debug time, inspect unfinished asyncio.all_tasks() and their stacks; task
names/stacks distinguish workers waiting on result capacity from provider,
artifact, callback, and producer tasks. Observer events and per-result timing
provide a less invasive production view.
Fix. Keep the consumer draining, move blocking callbacks off the event
loop, size input/output buffers for expected sink latency, and use total/batch
deadlines. In low-level streaming call start() before feeding and always call
finish(). Close the async generator explicitly when manually stopping early.
Related guide. Bounded Work and Backpressure.
An artifact record is incompatible
Symptom. Resume executes the provider instead of replaying a record, or a strict artifact operation reports incompatibility.
Likely cause. The compatibility fingerprint changed. It covers the item ID, prompt, participating context, and artifact identity, which can include model, provider, prompt version, parser version, and application version.
How to confirm. Compare the current prompt/context and ArtifactIdentity
with the artifact header and record fingerprint. Check whether code changed a
model route, parser schema, or application version.
Fix. Restore the compatible identity only if the behavior truly is compatible; otherwise allow execution and append a new record. Strictness prevents returning stale output after semantic changes.
Related guide. Results, Artifacts, and Resume.
CallableStrategy cannot prepare an artifact identity
Symptom. Artifact preparation fails before the callable runs and asks for
ArtifactIdentity.
Likely cause. A Python callable does not reveal a stable provider, model,
gateway route, parser version, or application version. Lambdas, closure IDs,
memory addresses, and repr() are unsafe replay identities.
How to confirm. Check that neither JsonlArtifactStore(identity=...) nor
CallableStrategy(identity=...) supplies an identity.
Fix. Provide a stable explicit identity and bump its version fields when prompting, parsing, routing, or application semantics change. A store identity overrides the strategy identity when both are present.
Related guide. Callable integration.
Resume did not call the provider
Symptom. A resumed item completes without an SDK/gateway invocation.
Likely cause. This is successful replay: a compatible record matched the selected resume policy.
How to confirm. Inspect result.replayed_from_artifact and the artifact
path/identity. Replayed usage remains available for audit but is excluded from
current-run consumption totals.
Fix. None when replay was intended. Use ResumePolicy.NONE for a live run,
or change a genuine compatibility component rather than fabricating a version
bump merely to bypass cache.
Related guide. Results, Artifacts, and Resume.
Resume did call the provider
Symptom. An item expected to replay instead executes again.
Likely cause. Its fingerprint or identity changed; the previous result was
a failure under REUSE_SUCCESSES; output persistence was disabled; the JSONL
record is malformed/unsupported; or the callable/model identity changed.
How to confirm. Check the resume policy, replayed_from_artifact, artifact
warnings, stored record type/output, and current identity. Compare prompt and
context byte-for-byte after their canonical serialization.
Fix. Use REUSE_ALL only when replaying prior terminal failures is desired.
Persist outputs required for replay and repair/replace malformed artifacts.
Do not weaken identity compatibility to force reuse.
Related guide. Results, Artifacts, and Resume.
Memory use grows
Symptom. Resident memory increases with a very large run.
Likely cause. process_prompts() retains the final BatchResult, an input
or output queue is unbounded, the application retains streamed results, or
concurrency permits many worker-local results. JsonlArtifactStore also loads
record history and builds replay indexes in memory when preparing an existing
artifact.
How to confirm. Check whether the code uses process_stream() with a lazy
source, positive max_queue_size and max_result_queue_size, and an
incremental sink. Measure application-held collections and artifact history,
not just queue sizes.
Fix. Stream and persist results instead of collecting them. Bound both queues, keep concurrency proportional to memory, discard consumer-held data, and rotate/compact long JSONL histories according to application retention policy. The approximate live result bound is result-queue capacity plus active workers plus the consumer's current item—not result-queue capacity alone.
Related guide. Bounded Work and Backpressure.
Progress is missing, noisy, or has a growing total
Symptom. progress=True logs instead of drawing a bar, totals increase, or
a custom callback fires much more often than the terminal display.
Likely cause. tqdm is not installed; a lazy producer continues accepting items; or the custom callback's per-item contract is being confused with the bundled reporter's throttled rendering.
How to confirm. Install/import tqdm and check the fallback notice. Inspect
ProcessorConfig.progress_refresh_interval_seconds separately from
progress_interval. Verify whether progress is True or a callable.
Fix. Install async-batch-llm[progress]. Tune
progress_refresh_interval_seconds for the bundled reporter; the logging
fallback uses at least a one-second cadence. Expect lazy-source totals to grow.
Aggregate inside a custom callback if its documented per-item delivery is too
frequent for an external metrics backend.
Related guide. Getting Started.
Is LLMCallPool an AI gateway?
Symptom. Code expects LLMCallPool to provide an HTTP endpoint, credential
store, provider routing, model catalog, or centralized policy plane.
Likely cause. Its compatibility name, LLMGateway, historically suggested
a network gateway. Both names refer to the same in-process shared executor.
How to confirm. LLMCallPool is LLMGateway; calls use the shared executor
directly and there is no background dispatcher or service.
Fix. Use LiteLLM or another real gateway for central routing/governance. Place ABL above it when an application run also needs ABL's bounded execution, recovery, deadlines, and artifacts.
Related guide. Single Call and Shared Call Pool and Choosing Between ABL and Alternatives.
Deprecation warnings after upgrading
Symptom. Code warns about timeout_per_item or calling
cache_hit_rate().
Likely cause. v0.20 uses attempt_timeout to make per-attempt semantics
explicit, and exposes cache_hit_rate as a property. Compatibility shims remain
so v0.18 applications can migrate incrementally.
How to confirm. Run tests with deprecations visible and locate
ProcessorConfig(timeout_per_item=...), .timeout_per_item, or
.cache_hit_rate().
Fix. Replace them with ProcessorConfig(attempt_timeout=...) and
.cache_hit_rate. The old forms remain supported without behavior changes in
v0.20 and are planned for removal in the next major release.
Related guide. Migrating from v0.18.x to v0.20.0.