Status: Active
Audience: Backend / SRE / on-call engineers
Scope: Stellar Horizon + Soroban RPC providers used by the YieldVault backend
Related: runbooks/RPC_FAILOVER.md (hands-on switch procedure),
MONITORING_OBSERVABILITY.md,
SERVICE_DEPENDENCY_MATRIX.md
This guide documents the strategy and configuration for RPC provider failover: how providers are ordered, how timeouts / retries / circuit breaking are tuned, and how an operator switches providers. It complements the step-by-step incident runbook in
runbooks/RPC_FAILOVER.md, which remains the authoritative "do this now" checklist during an outage.
Every deposit and withdrawal is submitted to the Stellar network through a
Soroban RPC endpoint (submitVaultOperation in backend/src/sorobanClient.ts).
If that endpoint is slow, rate-limited, or down, vault operations stall. To keep
the write path resilient the backend layers three independent protections —
retries, a retry budget, and a circuit breaker — in front of a
configurable provider endpoint that operators can re-point during an incident.
Providers are used in a strict priority order. The active provider is whichever
STELLAR_RPC_URL / STELLAR_HORIZON_URL currently points at; failover is the
act of promoting the next tier.
| Tier | Role | Example | When to use |
|---|---|---|---|
| 0 | Primary | Managed/paid provider (highest quota, lowest latency) | Default, steady state |
| 1 | Secondary | Independent managed provider on a different network/backbone | Primary degraded, rate-limited, or down |
| 2 | Tertiary | Self-hosted node or public community endpoint | Both managed providers unavailable |
| 3 | Read-only degrade | Horizon read endpoint only | Writes paused; serve balances/history while recovery is in progress |
Ordering principles
- Diversity over duplication. Tier 0 and Tier 1 must not share the same upstream operator or hosting region, or a single outage takes out both.
- Promote, never skip. Always fail over to the next tier in order so latency/quota characteristics degrade predictably.
- Prefer correctness to availability for writes. If no provider can be trusted (e.g. suspected fork / stale ledger), pause writes (Tier 3) rather than submit against an unhealthy node. See the circuit breaker below.
- Fail back deliberately. Return to Tier 0 only after it has been healthy for a sustained window (see §6), not on the first successful probe.
Record the concrete provider URLs for each tier in the secrets manager, not in this document. Keep at least Tier 0 and Tier 1 provisioned at all times.
The backend defends the RPC call path with three tunable layers. All are environment-driven so they can be adjusted per environment without a code change.
| Setting | Env var | Default | Notes |
|---|---|---|---|
| Max retries per operation | SOROBAN_MAX_RETRIES |
3 |
Applied around simulate/submit. |
| Base retry delay | SOROBAN_RETRY_DELAY_MS |
1000 ms |
Exponential backoff: delay = base × 2^attempt. |
| Transaction submit timeout | (in-code) | 300 s |
Stellar TransactionBuilder.setTimeout. |
Validation errors (bad input, simulation failures) are not retried — only transient/transport failures are. This prevents burning the retry budget on requests that will never succeed.
A global retry budget stops retries from amplifying an outage into a thundering-herd. Retries are only permitted while the recent success rate stays healthy.
| Setting | Env var | Purpose |
|---|---|---|
| Max retries in window | RETRY_BUDGET_MAX_RETRIES |
Hard cap on retries per rolling window. |
| Failure threshold | RETRY_BUDGET_FAILURE_THRESHOLD |
Failures before the budget tightens. |
| Minimum success rate | RETRY_BUDGET_MIN_SUCCESS_RATE |
Below this, retries are suppressed. |
| Rolling window | RETRY_BUDGET_WINDOW_MS |
Measurement window length. |
When failures cross the threshold the breaker opens and the API returns
503 Service Unavailable with a Retry-After header instead of piling more
load onto a failing provider. This is the signal to fail over.
| Setting | Env var | Purpose |
|---|---|---|
| Failure threshold | CIRCUIT_BREAKER_FAILURE_THRESHOLD |
Failures that trip the breaker open. |
| Measurement window | CIRCUIT_BREAKER_WINDOW_MS |
Window the threshold is measured over. |
| Cooldown | CIRCUIT_BREAKER_COOLDOWN_MS |
How long to stay open before a half-open probe. |
Interaction: per-request retries handle blips; the retry budget prevents
retry storms; the circuit breaker sheds load and surfaces a clean 503 once a
provider is genuinely unhealthy. A sustained open breaker is the trigger for the
provider switch in §4.
| Environment | SOROBAN_MAX_RETRIES |
SOROBAN_RETRY_DELAY_MS |
CIRCUIT_BREAKER_FAILURE_THRESHOLD |
CIRCUIT_BREAKER_COOLDOWN_MS |
|---|---|---|---|---|
| Production | 3 | 1000 | 5 | 30000 |
| Staging | 3 | 500 | 5 | 15000 |
| Local/dev | 1 | 250 | 10 | 5000 |
Tune from these baselines using the RPC latency/error dashboards; do not raise
SOROBAN_MAX_RETRIES to mask a provider that should be failed over instead.
The authoritative, copy-paste checklist lives in
runbooks/RPC_FAILOVER.md. At a glance:
- Confirm the primary is the problem — check the RPC error-rate/latency
dashboard and the circuit-breaker state; probe the backup with
curlbefore switching (getHealth/getLatestLedger). - Back up current config — snapshot
.envand log the previousSTELLAR_RPC_URLfor auditability. - Promote the next tier — set
STELLAR_RPC_URL(andSTELLAR_HORIZON_URLif applicable) to the next provider in the ordering table. - Reload, don't rebuild — restart / signal the process so the new endpoint is picked up; the circuit breaker resets on a healthy provider.
- Verify — submit a canary read and a low-value write; confirm the breaker stays closed and error rate returns to baseline.
- Announce — post status in the incident channel and update the status page per §7.
Provider ordering config: keep the ordered list of provider URLs in the secrets manager as e.g.
RPC_PROVIDER_TIER_0,RPC_PROVIDER_TIER_1,RPC_PROVIDER_TIER_2. The switch is then "copy Tier N intoSTELLAR_RPC_URL", which removes guesswork during an incident.
Fail over on signals, not vibes. Wire these to on-call:
- RPC error rate above baseline for N minutes.
- RPC p95 latency above the SLA in
SERVICE_DEPENDENCY_MATRIX.md. - Circuit breaker open (503s with
Retry-Afteron/vault/*). - Retry budget exhausted warnings in structured logs.
- Vault lifecycle audit failures spiking — the
vault.*.failedentries from the vault audit log (Issue #888) are a direct, product-level signal that submissions are not completing.
Return to a higher-priority provider only when all hold:
- Provider healthy on direct probes for a sustained window (≥ 15 min suggested).
- Circuit breaker closed and error rate at baseline on the current provider.
- No active incident depending on the current provider's state.
Fail back during low-traffic windows where possible, and watch the breaker for
one full CIRCUIT_BREAKER_WINDOW_MS after switching.
- Internal: announce switch start/finish in the incident channel with the from/to tier and the reason.
- External: if writes were paused or user-visible errors occurred, update
the status page and follow the incident comms flow in
incident_response_runbook.md. - Post-incident: capture provider, duration, and trigger in the postmortem
(
postmortem-playbook.md) and feed tuning changes back into §3.4.
- Tier 0 and Tier 1 providers provisioned with independent operators.
- Provider URLs stored in secrets manager as ordered tiers (not in git).
-
SOROBAN_MAX_RETRIES/SOROBAN_RETRY_DELAY_MSset per environment. - Retry-budget and circuit-breaker envs set per §3.4.
- Dashboards + alerts wired per §5.
-
runbooks/RPC_FAILOVER.mdreviewed and tested against the current provider list.