Skip to content

Robinhood transport: no backoff against a 100 req/min limit, and 429 propagates as a bare HTTPError #411

Description

@eaitbrahim

Split out of #198, now closed as superseded.

The gap

Robinhood documents 100 req/min sustained, 300 burst. RobinhoodTransport has no backoff, no retry, and no rate awareness anywhere — requests is used bare. A 429 propagates as an HTTPError alongside 401 and 5xx (transport.py:257).

Two call sites already note the cost in their own docstrings:

  • adapter.get_fee_summaryadapter.py:715: "Cost: 1 + N requests, where N is the number of history pages in the window. Against a 100 req/min sustained limit and a transport with no backoff, the bound matters" — worst case 21 requests per call (_MAX_PAGES = 20, plus the account read).
  • transport.get_orderstransport.py:505: "a transport with no backoff is a cost that grows with the account's age forever."

preview_order adds another call on top.

Why 429 must not be handled like the others

_request already draws one careful line — 404 becomes None, everything else raises — and transport.py:257 explains why swallowing more would be dangerous:

a transient 5xx or a dropped connection while polling a live order would then read exactly like "this order does not exist", and the adapter maps a None get_order result to a terminal FAILED status.

A 429 is different from both: it is neither "does not exist" nor a hard failure — it is "ask again shortly". Retrying it is safe for GETs. Retrying a create_order POST is not, until the idempotency issue lands: a 429 that the venue nonetheless acted on would be re-sent with a fresh client_order_id and place a second live order.

So the retry policy must be method-aware, and create_order must stay non-retrying until then.

Scope

  • Honour Retry-After when present; exponential backoff with jitter otherwise; bounded attempts.
  • GETs retry on 429 and connection errors. create_order does not retry on anything.
  • Reduce avoidable requests: get_fee_summary resolves the account twice (_account() is cached, so confirm whether this is still true after fix(brokers): sum robinhood's per-order fee_charged into a real fees_usd #222 — if it is, collapse it).
  • Tests: 429-then-200 on a GET, Retry-After honoured, exhausted attempts raise, and a 429 on create_order raises without a second POST.

Metadata

Metadata

Assignees

No one assigned

    Labels

    phase-3Execution/rails/agentprerequisiteBlocks other issues; do first

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions