[{"data":1,"prerenderedAt":7},["ShallowReactive",2],{"docs-errors\u002Fretry-safety":3,"docs-tabs-errors\u002Fretry-safety":6},{"content":4,"lastmod":5},"# What is safe to retry\n\nThe HTTP status and the word `retry` alone do not answer the main question: will\nretrying a write create a duplicate? Answering that needs two separate\ncharacteristics — whether the target call of the method managed to reach\nBitrix24, and whether the original request is a read or a write.\n\n`Retry-After` tells you when the next attempt is allowed from a load\nstandpoint. It does not prove that the previous write was not applied.\n\n## Terms\n\n### Target call\n\nThe target call is the Bitrix24 business method you wanted to execute, for\nexample `crm.deal.get` or `crm.deal.add`. A service-level OAuth token refresh\ndoes not count as a target call.\n\nFor `BH_APP_TIMEOUT`, the outcome is unknown. The code can come from the agent waiting 14 seconds for headers after sending the request or exhausting its 20-second total budget for a small response; from the Gateway waiting 30 seconds for a first frame or 15 seconds for a pre-commit stream continuation; or from the app itself returning `504` without `Content-Type`, a shape indistinguishable from an agent refusal. The app may have received the request and called Bitrix24. The code alone cannot establish whether it did.\n\n### Read and write\n\nA read does not change data: retrying it is generally safe with respect to\nduplicates. A write creates, changes, deletes, or sends data, so it needs a\nseparate verdict. Determine the operation type by the meaning of the method,\nnot the HTTP verb: a POST can perform a search, while methods like `*.add`,\n`*.update`, `*.delete`, and `*.send` change state.\n\n### Unknown outcome\n\n\"Unknown\" means the code alone cannot prove whether the target call managed to\nproduce an effect. The absence of a successful response does not equal the\nabsence of an effect.\n\n## Decision rule\n\n- **Retry is safe** — wait for the stated condition and retry with a limit on\n  the number of attempts.\n- **Read back first** — locate the operation's effect by a stable business key\n  or identifier. Retry the write only if you can prove the effect is absent.\n- **Do not retry automatically** — fix the cause, or hand the case to a human.\n  After the input changes, it is a new deliberate attempt, not a retry of the\n  same request.\n\nIf you cannot reliably verify the write's effect, \"read back first\" means \"do\nnot retry automatically\". `X-Request-Id` is for diagnostics and is not an\nidempotency key.\n\n## Code matrix\n\n| Code | Did the target call reach Bitrix24 | Read | Write | Condition and explanation |\n|---|---|---|---|---|\n| `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. |\n| `QUEUE_TIMEOUT` | No | Retry is safe | Retry is safe | Honor `Retry-After`. The call was removed from the queue before execution started. |\n| `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. |\n| `CALLER_RATE_LIMITED` | No | Retry is safe | Retry is safe | Honor `Retry-After` — until the one-minute window ends, the same refusal repeats. The platform refused before the operation started. |\n| `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. |\n| `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. |\n| `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. |\n| `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. |\n| `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. |\n| `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. |\n| `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. |\n| `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. |\n| `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. |\n| `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. |\n| `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`. |\n| `BH_APP_TIMEOUT` | Unknown | Retry is safe | Read back first | Sources: the agent's 14-second header window or 20-second total budget for a small response, the Gateway's 30-second first-frame or 15-second pre-commit idle window, or an app response of `504` without `Content-Type`. The request may have run. There is no `Retry-After`: waiting does not make retrying safe. |\n| `BH_APP_CONNECTION_LOST` | Unknown | Retry is safe | Read back first | The agent connected to the app, but the connection dropped before a response or the response body could not be read in full. The request may have run. No `Retry-After`: waiting does not make a retry safe. |\n| `AGENT_WRITE_ALREADY_EXECUTED` | No | — | Read back first | Arrives only for an AI agent key on a repeated `DELETE` to the same address within 30 seconds, never on reads. If `error.firstStatus` holds a success code, the first delete was executed — treat the operation as done. `null` means it is still running: read the record back. |\n| `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. |\n\n## Why some codes are ambiguous\n\n### `RATE_LIMITED`\n\nDelivery is unknown because the same code is shared by local limits and by\nBitrix24. Retrying a write after a pause is still safe: the known sources\neither never send the call or unambiguously reject it by rate without the\noperation being applied. This does not mean every HTTP 429 response always\noccurs before the call is sent.\n\n### `OPERATION_TIME_LIMIT`\n\nThe first rejection with this code is returned by a Bitrix24 method that was\nalready called. After it, Vibecode remembers the pause and may reject\nfollowing requests before they are sent. That is why the write path takes the\nmore conservative action — verify the effect first.\n\n### `TIMEOUT_QUARANTINE`\n\nThe current request, stopped by the local pause, is safe. An earlier operation\nwhose timeout triggered the quarantine may have an unknown outcome — this\nguarantee does not extend to it.\n\n### `DB_TRANSIENT`\n\nA transaction rollback in the Vibecode database is not a distributed rollback\nof Bitrix24. If the external call already completed, the local rollback does\nnot undo its effect.\n\n### `BH_APP_STARTING`\n\nNo response frame arrived from the app at all, or the agent answered for\nitself rather than the app. Both say a frame from the app was missing, but\nneither proves the app's code never saw the request: the connection could\nhave dropped after processing had already started. That is why a write\nrequires reading state back first rather than a blind retry — unlike\n`BH_APP_TIMEOUT` below, this code does carry a `Retry-After`.\n\n### `BH_APP_TIMEOUT`\n\nThis response can come from the agent waiting 14 seconds for headers after sending the request or exhausting its 20-second total budget for a small buffered response, or from the Gateway waiting 30 seconds for the first frame or seeing 15 seconds of stream silence before the response is committed. The app's own `504` without `Content-Type` has the same shape and becomes this code too. None of these sources proves that no side effect occurred: the app may have called Bitrix24. There is no `Retry-After`; check the result of a write before retrying it.\n\n### `BH_APP_CONNECTION_LOST`\n\nAn agent of version 1.3.41 or later connected to the app, and the connection dropped before a response, or the headers arrived but the body could not be read in full. The app is running, and the request may have reached it and called Bitrix24. There is no `Retry-After`. Check the result of a write before retrying it.\n\n## Algorithm for reads\n\n1. Recognize the response code.\n2. For an authorization error, a business error, or a circuit breaker, fix the\n   cause first.\n3. For a transient failure, honor `Retry-After`; if it is absent, apply\n   bounded exponential backoff with jitter.\n4. Retry the read no more than the number of times set by the client.\n5. On a persistent failure, stop and pass the time and `X-Request-Id` (if it\n   was present in the response) to support.\n\n## Algorithm for writes\n\n1. Do not use a single retryable-codes list for both reads and writes.\n2. If the \"Write\" cell allows a retry, wait for the stated condition and retry\n   with a limit on the number of attempts.\n3. If you need to read back the state, locate exactly the business effect of\n   the original operation by a unique attribute.\n4. If the effect is already there, treat the original write as successful and\n   do not retry it.\n5. If the effect is verifiably absent, perform a new attempt.\n6. If the check is unreliable, stop the automatic retry and resolve the case\n   manually.\n\nA plain GET without a unique criterion does not prove the absence of a\nduplicate. If the method has its own server-side idempotency key, you can rely\non that method's contract; this page itself does not introduce that guarantee.\n\n## Classifier example\n\nThe classifier takes the operation kind explicitly. It does not try to infer\nit from the HTTP verb and does not implement a universal `readBack`: verifying\nthe effect depends on the specific entity.\n\n```javascript\nconst VERIFY_WRITE_STATE = new Set([\n  'OPERATION_TIME_LIMIT', 'BITRIX_TIMEOUT', 'BITRIX_UNAVAILABLE',\n  'POOL_EXHAUSTED', 'DB_TRANSIENT', 'SERVICE_UNAVAILABLE', 'BH_APP_STARTING',\n  'BH_APP_TIMEOUT', 'BH_APP_CONNECTION_LOST', 'AGENT_WRITE_ALREADY_EXECUTED',\n]);\n\nconst FIX_BEFORE_RETRY = new Set([\n  'BITRIX_ERROR', 'TOKEN_REFRESH_FAILED', 'ERROR_LOOP_DETECTED',\n]);\n\nconst SAFE_RETRY = new Set([\n  'QUEUE_OVERFLOW', 'QUEUE_TIMEOUT', 'RATE_LIMITED', 'CALLER_RATE_LIMITED',\n  'TIMEOUT_QUARANTINE', 'LARGE_BODY_BACKEND_BUSY',\n]);\n\nfunction classifyRetry(code, operationKind) {\n  if (FIX_BEFORE_RETRY.has(code)) return 'fix_before_retry';\n  if (operationKind === 'write' && VERIFY_WRITE_STATE.has(code)) {\n    return 'state_verification_required';\n  }\n  if (SAFE_RETRY.has(code) || operationKind === 'read') return 'retry_with_limit';\n  return 'state_verification_required';\n}\n```\n\nHandle a code the classifier does not recognize conservatively: a read can be\nretried with bounded backoff, a write only after verifying the state. A\nnetwork error without the platform's JSON envelope has the same unknown\noutcome for a write.\n\nIn a batch, the decision is made separately for each item by its own code and\nthe sub-request's semantics. An HTTP 200 on the shared envelope does not mean\nevery item succeeded.\n\n## See also\n\n- [Error codes](\u002Fdocs\u002Ferrors)\n- [Limits, queues, and pauses](\u002Fdocs\u002Ferrors\u002Flimits)\n- [Bitrix24 and the platform](\u002Fdocs\u002Ferrors\u002Fplatform)\n- [Retries and error handling in code](\u002Fdocs\u002Ferrors\u002Fhandling)\n- [Batch](\u002Fdocs\u002Fbatch)\n- [App runtime environment](\u002Fdocs\u002Finfra\u002Fapp-runtime)\n","2026-10-22",{},1794739558023]