You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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_summary — adapter.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_orders — transport.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 Noneget_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.
Split out of #198, now closed as superseded.
The gap
Robinhood documents 100 req/min sustained, 300 burst.
RobinhoodTransporthas no backoff, no retry, and no rate awareness anywhere —requestsis used bare. A 429 propagates as anHTTPErroralongside 401 and 5xx (transport.py:257).Two call sites already note the cost in their own docstrings:
adapter.get_fee_summary—adapter.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_orders—transport.py:505: "a transport with no backoff is a cost that grows with the account's age forever."preview_orderadds another call on top.Why 429 must not be handled like the others
_requestalready draws one careful line — 404 becomesNone, everything else raises — andtransport.py:257explains why swallowing more would be dangerous: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_orderPOST is not, until the idempotency issue lands: a 429 that the venue nonetheless acted on would be re-sent with a freshclient_order_idand place a second live order.So the retry policy must be method-aware, and
create_ordermust stay non-retrying until then.Scope
Retry-Afterwhen present; exponential backoff with jitter otherwise; bounded attempts.create_orderdoes not retry on anything.get_fee_summaryresolves 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).Retry-Afterhonoured, exhausted attempts raise, and a 429 oncreate_orderraises without a second POST.