Repository navigation
[Bug]: USDT quote/send retries do not apply rate-limit cooldown #965
Description
Update — 2026-10-19, 3.0.1
Reproduced on iOS 3.0.1 (202) and Android 3.0.1 (195). Android is now runtime-confirmed affected. Both original pending transfers were later confirmed; the original 13:18 iOS send was matched to Android's receipt by transaction hash, sender, recipient and amount. A later 13:31 send was a separate retry/new payment. Detailed follow-up and sanitized logs. The original 3.0.0 findings below are retained as historical evidence.
What happened?
A mainnet USDT send on iOS 3.0.0 (200) shows “Too many requests. Try again shortly.” (Polish: “Zbyt wiele żądań. Spróbuj ponownie za chwilę.”). Existing iPhone logs confirm rate-limited quote and send attempts, followed by repeated quote failures within seconds. The amount screen restores its Continue action without a cooldown/countdown.
The message itself correctly corresponds to Core UsdtError.RateLimited; this is a retry/recovery issue, not the separate decimal-format bug.
Expected behavior
Foreground quote, maximum-quote and send operations should participate in appropriate rate-limit cooldown/backoff, coordinated with background requests for the same service. Respect provider retry timing where available and explain when the user can retry. Do not immediately re-enable repeated network attempts after a known rate limit. Preserve existing signed/pending-payment recovery and never initiate an unrelated second payment automatically.
Steps to Reproduce
Observed sequence, not a deterministic rate-limit trigger:
- On the mainnet USDT amount screen, request a quote for 5 USDT on Arbitrum One. The screenshot shows 10 USDT available.
- If the provider/Core returns
RateLimited, observe the error and retry behavior. - A send attempt in the same session also fails with
RateLimited. - Further quote attempts are allowed immediately and repeatedly return the same error.
Do not intentionally flood the public mainnet gateway to reproduce. A controlled 429/RPC quota-response test should assert shared cooldown and request counts for quote/max/send. No new payments were initiated during this investigation.
Logs / Screenshots / Recordings
Sanitized device-log excerpt (all eight relevant lines; UTC):
[2026-10-16 11:17:05.729 UTC] WARN⚠️: Failed to quote USDT: Zbyt wiele żądań. Spróbuj ponownie za chwilę. - UsdtSendView [UsdtSendView.swift: amountContent line: 337]
[2026-10-16 11:17:45.248 UTC] ERROR❌: BitkitCore.UsdtError.RateLimited - ServiceQueue: core [ServiceQueue.swift: queueError(from:service:wrapErrors:) line: 93]
[2026-10-16 11:17:45.253 UTC] WARN⚠️: Failed to send USDT: Zbyt wiele żądań. Spróbuj ponownie za chwilę. - UsdtSendView [UsdtSendView.swift: send() line: 670]
[2026-10-16 11:17:49.659 UTC] WARN⚠️: Failed to quote USDT: Zbyt wiele żądań. Spróbuj ponownie za chwilę. - UsdtSendView [UsdtSendView.swift: amountContent line: 337]
[2026-10-16 11:17:50.731 UTC] WARN⚠️: Failed to quote USDT: Zbyt wiele żądań. Spróbuj ponownie za chwilę. - UsdtSendView [UsdtSendView.swift: amountContent line: 337]
[2026-10-16 11:17:51.555 UTC] WARN⚠️: Failed to quote USDT: Zbyt wiele żądań. Spróbuj ponownie za chwilę. - UsdtSendView [UsdtSendView.swift: amountContent line: 337]
[2026-10-16 11:17:52.193 UTC] WARN⚠️: Failed to quote USDT: Zbyt wiele żądań. Spróbuj ponownie za chwilę. - UsdtSendView [UsdtSendView.swift: amountContent line: 337]
[2026-10-16 11:17:53.081 UTC] WARN⚠️: Failed to quote USDT: Zbyt wiele żądań. Spróbuj ponownie za chwilę. - UsdtSendView [UsdtSendView.swift: amountContent line: 337]
Existing device-log timeline (Europe/Warsaw, CEST; source log timestamps are UTC):
-13:17:05.729: Failed to quote USDT with the rate-limit message.
-13:17:45.248: BitkitCore.UsdtError.RateLimited.
-13:17:45.253: Failed to send USDT with the same message.
-13:17:49.659,50.731,51.555,52.193,53.081: five further rate-limited quote attempts.
The excerpt contains only these diagnostic lines, omitting unrelated wallet/network activity. Full raw wallet logs are not attached.
Bitkit Version
3.0.0(200), mainnet RC2; release head 59278ad871381f8ac4b9d4d0f70953643d59243e; Core 0.8.0-rc6.
Device / OS
iPhone 13 (iPhone14,5), physical device. iOS version not captured for this report.
Reproducibility
Observed repeatedly in one session when rate-limited. Frequency and the trigger of the initial rate limit are unknown. Android has the same code-level gap, but this rate-limit scenario was not reproduced there.
Additional context
Cause identified in release code
- UsdtWalletManager.refresh and waitForTransfer set/respect
rateLimitedUntil, using a 60-second cooldown afterRateLimited. - quote / quoteMaximum and send neither check that deadline nor set it when they encounter
RateLimited. beginOperation only guards wiping and tracks active operations. - UsdtSendView amountContent resets
busyafter a failed quote, enabling another attempt without response-aware retry timing. - Core Rpc.call_with_deadline returns
RateLimitedon HTTP429 or matching RPC quota/rate-limit errors. Its local 20-request burst / 80-per-minute pacing is not server-directed cooldown and does not consumeRetry-After. Available app logs do not reveal the exact upstream endpoint, HTTP status/RPC quota message or Retry-After that triggered this incident.
Android comparison
Android mainnet 3.0.0 (193), release head 19449fb0d729f51b9875eee2f66acfc0671e11da, also pins Core 0.8.0-rc6. UsdtRepo.refresh and waitForTransfer respect/set rateLimitedUntil. quote, quoteMaximum and send serialize via a mutex but do not check/set that cooldown; serialization alone does not provide backoff. UsdtViewModel.quote resets busy on failure, likewise permitting a subsequent attempt. This is source-confirmed parity, not an Android device reproduction.
Boundaries and related issues
The initial limiter trigger (app load, gateway/IP-wide traffic or upstream provider quotas) is not established. The logs do not prove eventual settlement or loss of funds. Investigate service-side quota pressure separately from the confirmed foreground recovery gap.
Separate from synonymdev/bitkit-core#171 and #964 (bridge minimum mapped to decimal-format error). Existing iOS #777/#874 concern Lightning startup retries, not USDT rate-limit recovery.

Activity
piotr-iohk commentedon Oct 19, 2026
3.0.1 physical-device follow-up — 2026-10-19
Reproduced during mainnet USDT-to-USDT testing on iPhone 13 / TestFlight 3.0.1 (202) and Samsung S22 SM-S901B / production 3.0.1 (195). The iOS build number is present in the session log; Android's installed package/version/build were verified. Both retain Core 0.8.0-rc6.
Android is now runtime-confirmed affected, extending the original report's code-only parity finding. Android logged
RateLimitedfor a send and wallet refreshes; iOS logged it for Paykit reconciliation, checking a pending USDT execution, and wallet refreshes. The screenshot reported pending $1 sends on both devices, with the rate-limit message visible on iOS.Sanitized diagnostic excerpt
The following is the complete selected diagnostic excerpt for this observation, converted to Europe/Warsaw (CEST). Addresses, transaction IDs, wallet contents and unrelated logs are omitted. Full device logs/databases are retained privately and are not attached.
Settlement correlation
The original 13:18 iOS send did settle: its stored USDT history record is
Confirmed, and its transaction hash, sender, recipient and 1.00 USDT amount match Android's 13:18 receipt exactly. Android's receipt details also showConfirmed. This is a transaction-level correlation, not just a match on time/amount.The original Android 13:19 send was separately observed as
Confirmedin its transaction details, and the iPhone history has the corresponding 13:19 incoming 1.00 USDT record markedConfirmed.The iPhone's successful 13:31 send is a distinct transaction, not evidence that the original recovered. The tester confirmed a retry/new payment. Do not count that later success as the original's settlement or claim that only one transfer occurred. Stored history timestamps are not a measurement of the settlement delay.
Scope of the finding
Retry-After. No provider outage or funds loss is established. Investigate service-side quota pressure separately from client retry coordination.No payment was initiated or retried by the agent; investigation consisted of log/history reads and opening existing receipt details. No code, release or milestone changes made.