# What is safe to retry

The HTTP status and the word `retry` alone do not answer the main question: will
retrying a write create a duplicate? 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 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.

### Read and write

A read does not change data: retrying it is generally safe with respect to
duplicates. 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. |
| `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. |
| `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 | 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. |
| `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. |
| `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. |
| `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 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.

### `BH_APP_CONNECTION_LOST`

An 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.

## Algorithm for reads

1. Recognize the response code.
2. For an authorization error, a business error, or a circuit breaker, fix the
   cause first.
3. For a transient failure, honor `Retry-After`; if it is absent, apply
   bounded exponential backoff with jitter.
4. Retry the read no more than the number of times set by the client.
5. 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

1. Do not use a single retryable-codes list for both reads and writes.
2. If the "Write" cell allows a retry, wait for the stated condition and retry
   with a limit on the number of attempts.
3. If you need to read back the state, locate exactly the business effect of
   the original operation by a unique attribute.
4. If the effect is already there, treat the original write as successful and
   do not retry it.
5. If the effect is verifiably absent, perform a new attempt.
6. 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.

```javascript
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', 'BH_APP_CONNECTION_LOST', 'AGENT_WRITE_ALREADY_EXECUTED',
]);

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', 'CALLER_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.

## See also

- [Error codes](/docs/errors)
- [Limits, queues, and pauses](/docs/errors/limits)
- [Bitrix24 and the platform](/docs/errors/platform)
- [Retries and error handling in code](/docs/errors/handling)
- [Batch](/docs/batch)
- [App runtime environment](/docs/infra/app-runtime)
