Dla agentów AI: markdown tej strony — /docs-content-en/errors/retry-safety.md indeks dokumentacji — /llms.txt
Artykuły dokumentacji są obecnie dostępne w języku angielskim.
What is safe to retry
The HTTP status and the word retry alone do not answer the main question: will
a retry create a duplicate record? Answering that needs two separate
characteristics — whether the target call of the method managed to reach
Bitrix24, and whether the original request is a read or a write.
Retry-After tells you when the next attempt is allowed from a load
standpoint. It does not prove that the previous write was not applied.
Terms
Target call
The target call is the Bitrix24 business method you wanted to execute, for
example crm.deal.get or crm.deal.add. A service-level OAuth token refresh
does not count as a target call.
For BH_APP_TIMEOUT, the request may or may not have reached the app: the
gateway only knows that it handed the frame to the tunnel, not that the app
received it — the agent may have failed to connect to the app's port. The
gateway simply stopped waiting for a response on its own timeout, not because
the app refused. So the outcome of the business operation is unknown from the
code alone: if the request did reach the app, it may have called Bitrix24
either before or after the gateway stopped waiting.
Read and write
A read does not change data: retrying it is generally safe from a duplicate
standpoint. A write creates, changes, deletes, or sends data, so it needs a
separate verdict. Determine the operation type by the meaning of the method,
not the HTTP verb: a POST can perform a search, while methods like *.add,
*.update, *.delete, and *.send change state.
Unknown outcome
"Unknown" means the code alone cannot prove whether the target call managed to produce an effect. The absence of a successful response does not equal the absence of an effect.
Decision rule
- Retry is safe — wait for the stated condition and retry with a limit on the number of attempts.
- Read back first — locate the operation's effect by a stable business key or identifier. Retry the write only if you can prove the effect is absent.
- Do not retry automatically — fix the cause, or hand the case to a human. After the input changes, it is a new deliberate attempt, not a retry of the same request.
If you cannot reliably verify the write's effect, "read back first" means "do
not retry automatically". X-Request-Id is for diagnostics and is not an
idempotency key.
Code matrix
| Code | Did the target call reach Bitrix24 | Read | Write | Condition and explanation |
|---|---|---|---|---|
QUEUE_OVERFLOW |
No | Retry is safe | Retry is safe | Honor Retry-After, lower concurrency, use backoff with jitter. The queue rejected the call before it ran. |
QUEUE_TIMEOUT |
No | Retry is safe | Retry is safe | Honor Retry-After. The call was removed from the queue before execution started. |
RATE_LIMITED |
Unknown | Retry is safe | Retry is safe | Honor Retry-After. The code covers both local pre-call limits and a Bitrix24 rate rejection without the operation being applied. |
OPERATION_TIME_LIMIT |
Unknown | Retry is safe | Read back first | Strictly honor Retry-After. The first rejection comes from a method already called on Bitrix24; subsequent ones may occur locally before the call is sent. |
TIMEOUT_QUARANTINE |
No | Retry is safe | Retry is safe | Do not retry before the full Retry-After elapses: early requests interfere with recovery. The current call was stopped locally before it was sent. |
BITRIX_TIMEOUT |
Yes | Retry is safe | Read back first | Wait for the recommended pause. The platform stopped waiting for the response, but processing of the write on Bitrix24 may have continued. |
BITRIX_ERROR |
Yes | Retry is safe after fixing the cause | Do not retry automatically | Fix the cause using b24Code, message, and the error fields. A plain pause does not change the outcome; for a questionable write, additionally verify the state. |
BITRIX_UNAVAILABLE |
Unknown | Retry is safe | Read back first | Use bounded exponential backoff. The code covers upstream 5xx and network failures; the absence of a response does not prove the absence of an effect. |
POOL_EXHAUSTED |
Unknown | Retry is safe | Read back first | Honor Retry-After. The shared DB error handler can fire at different stages of request processing. |
DB_TRANSIENT |
Unknown | Retry is safe | Read back first | Honor Retry-After. Rolling back the local DB transaction does not roll back a Bitrix24 call that already happened. |
SERVICE_UNAVAILABLE |
Unknown | Retry is safe | Read back first | Honor Retry-After. The platform boundary does not know whether the backend managed to complete the external effect; in particular, a 504 does not stop the upstream call. |
TOKEN_REFRESH_FAILED |
No | Retry is safe after authorization is restored | Retry is safe after authorization is restored | The target method was not sent. A plain backoff without fixing authorization is useless. |
ERROR_LOOP_DETECTED |
No | Retry is safe after the cause is resolved | Do not retry automatically | The current call was stopped by the circuit breaker before it was sent. Fix the stable cause first; occasional probe calls may get through. |
BH_APP_STARTING |
Unknown | Retry is safe | Read back first | No response frame arrived from the app at all, or the agent answered for itself rather than the app — both say a frame from the app was missing, but neither proves the app's code never saw the request: the connection could have dropped after processing had already started. Wait for Retry-After. |
BH_APP_TIMEOUT |
Unknown | Retry is safe | Read back first | The gateway stops waiting for a response under one of two windows (30 seconds with no frame at all, or an already-started response going silent for more than 15 seconds) — not because the app refused. The request may have been delivered to the app and executed, or it may never have arrived: the gateway only knows that it handed the frame to the tunnel. There is no Retry-After in the response: waiting does not make the retry safe. |
LARGE_BODY_BACKEND_BUSY |
No | Retry is safe | Retry is safe | Honor Retry-After. The admission gate rejected the request before the route handler and the target call. |
Why some codes are ambiguous
`RATE_LIMITED`
Delivery is unknown because the same code is shared by local limits and by Bitrix24. Retrying a write after a pause is still safe: the known sources either never send the call or unambiguously reject it by rate without the operation being applied. This does not mean every HTTP 429 response always occurs before the call is sent.
`OPERATION_TIME_LIMIT`
The first rejection with this code is returned by a Bitrix24 method that was already called. After it, Vibecode remembers the pause and may reject following requests before they are sent. That is why the write path takes the more conservative action — verify the effect first.
`TIMEOUT_QUARANTINE`
The current request, stopped by the local pause, is safe. An earlier operation whose timeout triggered the quarantine may have an unknown outcome — this guarantee does not extend to it.
`DB_TRANSIENT`
A transaction rollback in the Vibecode database is not a distributed rollback of Bitrix24. If the external call already completed, the local rollback does not undo its effect.
`BH_APP_STARTING`
No response frame arrived from the app at all, or the agent answered for
itself rather than the app. Both say a frame from the app was missing, but
neither proves the app's code never saw the request: the connection could
have dropped after processing had already started. That is why a write
requires reading state back first rather than a blind retry — unlike
BH_APP_TIMEOUT below, this code does carry a Retry-After.
`BH_APP_TIMEOUT`
This response is formed when the gateway stops waiting for the app under one
of two windows: 30 seconds with no frame at all, or an already-started
response (headers arrived) going silent for more than 15 seconds. Neither
case proves the request reached the app — the gateway only knows that it
handed the frame to the tunnel, not that the app received it. If the request
did arrive, the app may have called Bitrix24, so the outcome of the operation
is unknown under the BH_APP_TIMEOUT code, and there is no Retry-After in
the response: waiting before retrying does not make the retry safe.
Algorithm for reads
- Recognize the response code.
- For an authorization error, a business error, or a circuit breaker, fix the cause first.
- For a transient failure, honor
Retry-After; if it is absent, apply bounded exponential backoff with jitter. - Retry the read no more than the number of times set by the client.
- On a persistent failure, stop and pass the time and
X-Request-Id(if it was present in the response) to support.
Algorithm for writes
- Do not use a single retryable-codes list for both reads and writes.
- If the "Write" cell allows a retry, wait for the stated condition and retry with a limit on the number of attempts.
- If you need to read back the state, locate exactly the business effect of the original operation by a unique attribute.
- If the effect is already there, treat the original write as successful and do not retry it.
- If the effect is verifiably absent, perform a new attempt.
- If the check is unreliable, stop the automatic retry and resolve the case manually.
A plain GET without a unique criterion does not prove the absence of a duplicate. If the method has its own server-side idempotency key, you can rely on that method's contract; this page itself does not introduce that guarantee.
Classifier example
The classifier takes the operation kind explicitly. It does not try to infer
it from the HTTP verb and does not implement a universal readBack: verifying
the effect depends on the specific entity.
const VERIFY_WRITE_STATE = new Set([
'OPERATION_TIME_LIMIT', 'BITRIX_TIMEOUT', 'BITRIX_UNAVAILABLE',
'POOL_EXHAUSTED', 'DB_TRANSIENT', 'SERVICE_UNAVAILABLE', 'BH_APP_STARTING',
'BH_APP_TIMEOUT',
]);
const FIX_BEFORE_RETRY = new Set([
'BITRIX_ERROR', 'TOKEN_REFRESH_FAILED', 'ERROR_LOOP_DETECTED',
]);
const SAFE_RETRY = new Set([
'QUEUE_OVERFLOW', 'QUEUE_TIMEOUT', 'RATE_LIMITED',
'TIMEOUT_QUARANTINE', 'LARGE_BODY_BACKEND_BUSY',
]);
function classifyRetry(code, operationKind) {
if (FIX_BEFORE_RETRY.has(code)) return 'fix_before_retry';
if (operationKind === 'write' && VERIFY_WRITE_STATE.has(code)) {
return 'state_verification_required';
}
if (SAFE_RETRY.has(code) || operationKind === 'read') return 'retry_with_limit';
return 'state_verification_required';
}
Handle a code the classifier does not recognize conservatively: a read can be retried with bounded backoff, a write only after verifying the state. A network error without the platform's JSON envelope has the same unknown outcome for a write.
In a batch, the decision is made separately for each item by its own code and the sub-request's semantics. An HTTP 200 on the shared envelope does not mean every item succeeded.