Skip to content

[Bug]: USDT quote/send retries do not apply rate-limit cooldown #965

Closed

Description

@piotr-iohk

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:

  1. On the mainnet USDT amount screen, request a quote for 5 USDT on Arbitrum One. The screenshot shows 10 USDT available.
  2. If the provider/Core returns RateLimited, observe the error and retry behavior.
  3. A send attempt in the same session also fails with RateLimited.
  4. 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

iOS USDT amount screen displaying a rate-limit error

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 after RateLimited.
  • 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 busy after a failed quote, enabling another attempt without response-aware retry timing.
  • Core Rpc.call_with_deadline returns RateLimited on 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 consume Retry-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

  1. added theissue type on Oct 16, 2026
  2. added this to the 3.0.5 milestone on Oct 16, 2026
  3. piotr-iohk commented on Oct 19, 2026

    @piotr-iohk
    CollaboratorAuthor

    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 RateLimited for 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.

    # USDT rate-limit evidence — 2026-10-19
    All times below are Europe/Warsaw (CEST). Wallet addresses, transfer identifiers and unrelated logs omitted.
    
    Android 3.0.1 (195):
    13:14:47.480 USDT send failed: RateLimited
    13:18:31.213 USDT refresh failed: RateLimited
    13:19:39.479 USDT refresh failed: RateLimited
    
    iOS (current TestFlight session):
    13:14:47.105 Paykit USDT reconciliation: RateLimited
    13:14:57.161 USDT refresh: rate limited
    13:18:30.879 Pending USDT execution check: rate limited
    13:19:39.411 USDT refresh: rate limited
    13:20:42.098 USDT refresh: rate limited
    
    Settlement checks (read-only; observed at approximately 13:34–13:35 CEST):
    iOS original 13:18:30 outgoing 1.00 USDT: stored status Confirmed.
    Android matching 13:18 incoming 1.00 USDT: detail status Confirmed.
    Transaction hash, sender and recipient match exactly between the two records; values omitted.
    Android original 13:19 outgoing payment: detail status Confirmed (verified in earlier read-only inspection).
    iOS 13:31 outgoing payment: separate stored transfer, status Confirmed; tester confirmed a retry/new payment.
    

    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 show Confirmed. This is a transaction-level correlation, not just a match on time/amount.

    The original Android 13:19 send was separately observed as Confirmed in its transaction details, and the iPhone history has the corresponding 13:19 incoming 1.00 USDT record marked Confirmed.

    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

    • This reproduces rate limiting on 3.0.1 and confirms Android runtime impact; it is not evidence that 3.0.1 introduced a new regression. Related source revisions: iOS 202 and Android 195.
    • Refresh and pending-execution polling already apply roughly 60-second backoff. The repeated refresh warnings are separated by at least that interval; this observation does not establish a tight-loop background retry bug.
    • Foreground quote/max/send still do not share that cooldown. The original foreground recovery finding remains applicable, while this session additionally shows rate limiting can delay pending-state reconciliation and wallet refresh.
    • Available app logs do not identify the limiting upstream service, HTTP/RPC quota response, quota scope, or 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions