Troubleshooting and FAQ
Start with the terminal WorkItemResult, its error_category (look up any value in
the ErrorCategory reference), and
result.timing. Enable 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 RetryConfig.max_rate_limit_retries exhaustion (the item fails with
error_category="rate_limit_retries_exceeded"). Inspect gateway
logs for hidden retries beneath each ABL attempt. If cooldowns follow
application exceptions rather than provider responses, check how they are
classified: an untyped exception without a status code whose message contains
429, resource_exhausted, quota exceeded or rate limit is treated as a
rate limit. A built-in programming error (ValueError, TypeError, …) is
logic_error and isn't retried, whatever its message. To signal a real rate
limit from your own code, raise an exception with status_code = 429; otherwise
pass an explicit error_classifier.
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.
Items wait before provider capacity even though connections are free
Cause. A scoped RPM/TPM gate admits work before provider capacity. This is intentional: no connection or provider slot is held while a quota refills.
Confirm and fix. Compare result.quota_wait_seconds with
result.admission_wait_seconds and inspect quota wait percentiles. Tune the
real RPM/TPM budget or estimator; adding connections cannot overcome quota.
Token estimate exceeds the configured per-minute limit
Cause. One estimated input-plus-output total is larger than
max_tokens_per_minute, so it can never become admissible by waiting.
quota_burst_seconds doesn't affect this check.
Fix. Correct an inflated estimator, reduce the expected output allowance, raise the limit to the actual account budget, or route the call to a genuinely independent quota scope. Do not split one provider request merely to satisfy a local configuration unless the application semantics permit it.
TPM limiting appears conservative after timeouts or 429s
Cause. Once provider work starts, unknown usage retains its token reservation. A timeout, transport error, or 429 without reliable usage is not the same as an explicit zero-token response.
Confirm and fix. Monitor unknown_usage_attempts and reconciliation
events. Extract provider-reported failed usage when available. Otherwise keep
the conservative accounting or increase headroom based on measured behavior;
do not treat missing usage as zero.
Two providers unexpectedly share a quota
Cause. Their strategies return the same quota_scope identity, often
because quota scope defaults to a shared concurrency scope.
Fix. Give genuinely independent accounts distinct stable scope objects. Keep them shared if the upstream provider or gateway actually enforces one account budget.
Two models do not share an account quota
Cause. Separate strategy/model instances default to separate scopes even though the provider bills them against one account-level quota.
Fix. Pass the same quota_scope object to both strategies. Scope sharing
also coordinates cooldown, so verify the ownership matches the upstream
account rather than just the model name.
Large items delay small items
Cause. Quota admission is FIFO. A large estimate at the head waits for enough tokens and later small estimates do not bypass it indefinitely.
Fix. Improve estimates and expected-output bands. Separate workloads only when they spend independent upstream quotas; otherwise head-of-line waiting is the fairness trade-off of one shared budget.
Gateway retrying makes reported usage differ from ABL attempts
Cause. A gateway may perform several upstream attempts inside one visible
strategy.execute() call. ABL can account only for the usage the gateway
returns.
Fix. Choose one strong retry owner, inspect gateway logs, and configure the gateway to return aggregate usage. Do not claim per-upstream-attempt visibility when the gateway hides it.
Dry-run or replay has no quota events
Cause. This is the contract. Dry-run, compatible replay, and middleware-filtered items perform no live admission and mutate no RPM/TPM state.
Fix. None. Test quota behavior with a credential-free live fake strategy instead of using dry-run or replay as a quota simulation.
Why quota wait is separate from admission wait
Quota wait measures cooldown-following RPM/TPM admission. Admission wait measures provider-capacity semaphore wait. Keeping them separate shows whether the account budget or transport/provider capacity is the bottleneck and prevents double-counting in summaries.
Related guide. Token-Aware Admission.
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, and check timing.timeout_category
(for example admission_timeout when a deadline hit during the provider-capacity
wait). 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, Budgets 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, a Gemini safety block or OpenAI refusal,
length termination with no text, a Chat Completions response containing only
tool calls, or an actually empty choice. On OpenAIModel's Responses API
(the default since v0.27), a function-call-only reply instead succeeds with
empty text and result.tool_calls, so check for text == "" in a parser.
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; the callable/model identity changed; or the
previous result's category is never replayed under any policy. Those are
configuration failures (token_estimator_required, token_estimation_error,
token_estimate_exceeds_limit, quota_scope_error), middleware_filtered,
artifact_serialization_error, and guardrail audit records (batch_aborted,
batch_deadline_exceeded, batch_budget_exceeded,
framework_total_item_timeout); see
Middleware and replay.
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;
it replays them except for the non-replayable categories above.
Persist outputs required for replay and repair/replace malformed artifacts.
Do not weaken identity compatibility to force reuse.
Related guide. Results, Artifacts, and Resume.
OpenAIModel rejects a request field or can't reach its server
Symptom. After upgrading to v0.27, OpenAIModel raises
ValueError: OpenAI Responses: ... before any request, or calls to a custom
base_url fail with 404.
Likely cause. OpenAIModel now uses the Responses API. Chat-only fields
(n, stop, seed, penalties, logit_bias, legacy functions) have no
Responses equivalent, and servers other than OpenAI often don't implement
/responses.
How to confirm. The ValueError names the field. For a 404, check whether
the model's base_url (or OPENAI_BASE_URL) points somewhere other than OpenAI.
Fix. Pass api_surface="chat_completions" to keep Chat Completions. For
non-OpenAI servers, use OpenAICompatibleModel or
llm("openai-compatible:<model>", base_url=...).
Related guide. OpenAI Integration and the v0.27 migration guide.
A run stopped with budget_exceeded
Symptom. batch.termination.kind == "budget_exceeded" and some items end
with batch_budget_exceeded.
Likely cause. A configured max_total_tokens or max_total_cost was
reached, or the cost_function raised or returned an invalid value (the
termination reason says which).
How to confirm. Read batch.termination.reason and, from a processor,
get_stats(): budget_tokens_used, budget_cost_used, and
budget_cost_complete (False means pricing failed).
Fix. Raise the cap, fix the cost function, or resume from a checkpoint: completed items replay without being counted again. Unfinished items re-run.
Related guide. Token and cost budgets.
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 former name, LLMGateway (deprecated since v0.27),
suggested a network gateway. It is an in-process shared executor.
How to confirm. 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 raises a DeprecationWarning or UserWarning after upgrading.
Likely cause. A compatibility shim is still in use:
- v0.20:
timeout_per_item(nowattempt_timeout) and callingcache_hit_rate()(now a property). - v0.27:
LLMGateway,execute()returning a 2-tuple,effective_input_tokens()/estimated_cost()withoutcached_token_rate,BatchProcessor,ProcessingStats, andgrounding_metadata_extractor.
How to confirm. Run tests with -W error::DeprecationWarning, or read
pytest's warnings summary, and locate the deprecated names. On Python 3.14 with
google-genai installed, add -W "ignore::DeprecationWarning:google.genai.types" after
the error filter; google-genai emits its own warning at import.
Fix. Use the replacements in the
v0.27 deprecations table; for the v0.20 names,
use ProcessorConfig(attempt_timeout=...) and .cache_hit_rate. The v0.27 names
are removed in 1.0; the v0.20 forms are slated for removal in the next major
release.
Related guide. Migrating to v0.27 and Migrating from v0.18.x to v0.20.0.