Purpose: Detect, escalate, mitigate, and communicate incidents where user
vault withdrawals fail to complete
Severity default: SEV-2 (funds not lost, but user cannot access funds) —
escalate to SEV-1 if funds appear debited without being received
RTO Target: 30 minutes to mitigation (writes restored or safely paused)
Owner: Backend on-call
Last Updated: July 25, 2026
Related:
ERROR_CODE_TROUBLESHOOTING.md ·
RPC_FAILOVER.md ·
../RPC_PROVIDER_FAILOVER_STRATEGY.md ·
BACKEND_REDEPLOY.md ·
../incident_response_runbook.md ·
../postmortem-playbook.md
A "failed withdrawal" is any request to
POST /api/v1/vault/withdrawalsthat does not reach a durableconfirmedstate — whether it errors, times out, or leaves the user unsure whether their funds moved. Because withdrawals move user funds, communicate early and never guess about fund state — verify on-chain.
Use this playbook when any of the following are observed:
- Spike in
vault.withdrawal.failedentries in the vault lifecycle audit log (Issue #888) or in5xx/503responses from/vault/withdrawals. - Users report withdrawals stuck in
pendingor erroring out. - Circuit breaker open on the Soroban write path (
503+Retry-After). - Withdrawals succeed on-chain but balances/history do not update (persistence or reconciliation gap).
- A withdrawal appears debited on-chain but not received by the user → treat as SEV-1 immediately and page the incident commander.
| Role | Responsibility |
|---|---|
| Incident Commander (IC) | Owns the incident, declares severity, coordinates. |
| Backend on-call | Diagnosis and mitigation (this playbook). |
| Comms lead | Status page + user/support messaging (§7). |
| Contract/chain SME | On-chain verification, called in for suspected fund-state issues. |
For SEV-2 the backend on-call may hold IC + engineer roles; SEV-1 requires a dedicated IC.
Confirm the incident and gather scope before acting.
- Vault audit log (fastest product-level signal). Query the vault
lifecycle audit trail (Issue #888) for recent failures:
- Filter
operation = withdrawal,outcome = failure. - Note the dominant
errorCode— it points directly at the failure class (see §4). Codes:SOROBAN_CIRCUIT_OPEN,SOROBAN_SIMULATION_ERROR,IDEMPOTENCY_CONFLICT,VAULT_OPERATION_ERROR. - Capture
correlationId/traceIdfor a representative failure to trace.
- Filter
- Metrics/dashboards. Check
/vault/withdrawalserror rate & latency, the circuit-breaker state, and RPC error/latency panels. - Logs. Search structured logs by the captured
correlationId/traceIdfor the full request path. - Scope. Determine: all withdrawals or a subset? One wallet or many? Since when? Correlate the start time with deploys, migrations, or RPC changes.
Record findings in the incident channel as you go.
Match the dominant errorCode / symptom to the row below.
- Meaning: The Soroban write path is shedding load; the RPC provider is failing or slow.
- Mitigate: Fail over the RPC provider — follow
RPC_FAILOVER.mdand the ordering/timeout strategy in../RPC_PROVIDER_FAILOVER_STRATEGY.md. Once a healthy provider is active the breaker resets and withdrawals resume.
- Meaning: The transaction failed simulation/validation (e.g. timelock not elapsed, insufficient shares, contract state needs restore).
- Mitigate: Use
ERROR_CODE_TROUBLESHOOTING.mdwithdrawal patterns (timelock, insufficient shares, ledger restore). This is usually per-request, not a systemic outage — do not fail over RPC. Guide the user/support with the specific remediation.
- Meaning: The same
Idempotency-Keywas reused with a different payload, or a retry raced an in-flight request. - Mitigate: Confirm whether the original operation succeeded (audit log / on-chain). If it did, the user's withdrawal is fine — reassure. If not, have the client retry with a fresh idempotency key.
- Meaning: Unhandled backend error (DB unavailable, bug, dependency down).
- Mitigate: Check DB health (
DATABASE_RESTORE.md) and recent deploys. If a recent deploy correlates, roll back (§6).
- Meaning: The tx confirmed on-chain but the DB transaction/vault state did not update (crash between submit and persist).
- Mitigate: Do not let the user re-submit. Run the position
reconciliation job to re-sync state from chain, and use
REPLAY_PROCEDURES.mdto replay the missed event. Verify the audit log showssubmittedwithout a matchingconfirmed.
| Condition | Action |
|---|---|
| Failures isolated to one class and mitigable in-band | Backend on-call handles; keep IC informed. |
| Broad outage (all withdrawals failing) > 15 min | Page IC; declare SEV-2. |
| Funds debited on-chain but not received, or any suspected loss | Declare SEV-1; page IC + contract SME immediately; pause withdrawals (§6). |
| Suspected contract/security issue | Page security on-call; follow ../incident_response_runbook.md. |
Escalate on time as well as symptom: if not mitigated within the 30-minute RTO, raise severity and pull in more responders.
Prefer the least invasive action that stops user harm.
- RPC failover (§4.1) — for provider-driven failures; no deploy needed.
- Pause withdrawals — if fund safety is uncertain, stop new withdrawals while investigating. Use the maintenance-mode / feature-flag controls to gate the withdrawal route so users get a clear "temporarily paused" response instead of ambiguous errors. Always prefer pausing to risking double-spend or loss.
- Roll back — if a recent deploy correlates with the onset, redeploy the
last known-good build per
BACKEND_REDEPLOY.md. - Hotfix — for a clearly identified bug with a small, tested fix: ship behind a flag where possible, verify on staging, then deploy.
- Reconcile — after service is restored, run reconciliation/replay (§4.5) so any in-flight withdrawals reach a correct terminal state.
Verification after any mitigation: submit a low-value canary withdrawal;
confirm a clean vault.withdrawal.confirmed audit entry, updated balance, and
breaker closed. Re-enable withdrawals only after the canary passes.
- T+0 (acknowledge): Post in the incident channel — what's failing, scope, severity, IC. If user-facing, publish a status-page notice ("Some withdrawals may be delayed — funds are safe; investigating").
- During: Update at a fixed cadence (every 15–30 min) even if "no change." Give support a canned, accurate holding message. Never tell a user funds are safe until verified on-chain.
- Mitigated: Announce restoration; note whether affected withdrawals need to be retried by users or were reconciled automatically.
- Resolved: Close the incident; open a postmortem
(
../postmortem-playbook.md) using thetemplates/post-mortem.mdandtemplates/incident-report.mdtemplates.
- Publish postmortem with timeline, root cause, and action items.
- File follow-ups (alerting gaps, missing guardrails, flaky dependency).
- Confirm every affected withdrawal reached a correct terminal state
(reconciled or retried) — reconcile the audit log
submittedvsconfirmedcounts to zero out orphans. - Feed any timeout/threshold learnings back into
../RPC_PROVIDER_FAILOVER_STRATEGY.md§3.4. - Update this playbook if a new failure class was discovered.
Symptom / errorCode |
First action | Runbook |
|---|---|---|
SOROBAN_CIRCUIT_OPEN, 503s |
Fail over RPC provider | RPC_FAILOVER |
SOROBAN_SIMULATION_ERROR |
Per-request troubleshooting (timelock/shares) | ERROR_CODE_TROUBLESHOOTING |
IDEMPOTENCY_CONFLICT |
Verify original succeeded; retry w/ new key | this doc §4.3 |
VAULT_OPERATION_ERROR / 5xx |
Check DB + recent deploys; roll back | BACKEND_REDEPLOY |
| On-chain OK, state stale | Reconcile + replay; do not re-submit | REPLAY_PROCEDURES |
| Funds debited, not received | SEV-1, pause withdrawals, page SME | incident_response_runbook |