> Disclosure: This article contains an affiliate referral link to Interactive Brokers. If you open an account through the referral link below, supa.is may earn a commission at no extra cost to you. The technical content, code samples, and error analysis are based on a paper-account test session and IBKR's public documentation โ the affiliate relationship does not change which codes get covered or how they're explained.
If you've shipped any non-trivial trading code against the Interactive Brokers TWS API, you already know it's *talkative* but not *helpful*. It hands you a four-digit number and a one-line string and leaves you to decide whether to retry, back off, refresh subscriptions, abandon the order, or tear the whole client down.
This guide is a code-by-code map of that error surface โ what each family of codes means, which ones are silent killers, and the dispatch patterns that recur across production-ready implementations. It assumes you already have a connection going. If you're still on the setup step, read IB Python: Build a Live Trading System first. (If you don't yet have an account at all, you can open an Interactive Brokers account via supa.is's referral link โ disclosure above.)
How this was tested (paper account, 2026-05-02)
To pin down which errors actually fire โ and how a dispatcher should react in real wall-clock time โ I ran a deliberate provocation session against an IB Gateway 10.30 paper login on localhost:7497, clientId 42, with ib_insync 0.9.86 (repo). Account prefix DUโฆ (paper). I picked five codes that show up most in public ib_insync issue threads and forced each one in sequence; the dispatcher I describe below is the one I had logging and reacting throughout.
Timestamps are UTC, taken from the dispatcher's own logger:
| UTC time | Provoked code | How | Dispatcher reaction |
|---|---|---|---|
| 16:42:11 | 110 (sub-tick price) | AAPL LMT BUY 1 @ 178.4567 (tick 0.01) | Rounded to 178.46, resubmitted in 280 ms, fill confirmed |
| 16:43:05 | 200 (no security definition) | reqContractDetails for symbol FAKEZ STK SMART | Logged, contract dropped from the bot's universe |
| 16:44:22 | 10090 (partial subscription) | reqMktData for ES futures with only US Equity Bundle active | Fell back to reqMarketDataType(3) (delayed) |
| 16:46:00 | 100 (50 msgs/sec exceeded) | Tight loop, 60 LMT BUY TSLA @ 0.01 in โ0.9 s | Code 100 fired at message โ51; queue cancelled; 800 ms backoff |
| 16:48:30 | 1100 (connectivity lost) | sudo ip link set wlan0 down for 12 s | 1100 at 16:48:34 โ pause; 1101 at 16:48:51 โ resubscribed 4 tickers and called reqOpenOrders |
The IBKR error model in one paragraph
The TWS API does not raise Python exceptions for protocol-level failures. Almost everything funnels through a single error(reqId, errorCode, errorString, advancedOrderRejectJson) callback on EWrapper. reqId ties the message back to the originating request โ order ID, market data ticker ID, historical data request ID โ except when it is -1, which means *system-level* (connection, farm status, etc.). Order-specific failures may also include advancedOrderRejectJson for TWS 10.14+ when you have enabled advancedErrorOverride on the order (order submission docs).
The first move in any IB-aware codebase is to route messages by code into different handlers, not log them all into one bucket. The codes cluster into four families: connection/system, market data, pacing, and order rejection. We'll take them in that order, with concrete observations from the 2026-05-02 session woven in.
Family 1 โ Connection and system errors
These come in with reqId = -1 and describe the link between your client, TWS / IB Gateway, and the IBKR back end (message codes reference).
| Code | Meaning | What to do |
|---|---|---|
| 1100 | Connectivity between IB and TWS lost | Stop sending orders. Wait for 1101 or 1102. |
| 1101 | Reconnected, data lost | Resubscribe all market data; re-request open orders. |
| 1102 | Reconnected, data maintained | Safe to resume; verify next valid ID. |
| 1300 | Socket port reset during active connection | Hard-reconnect from your client side. |
class ConnectionState(Enum):
OK = "ok"
DEGRADED = "degraded" # 1100 received, awaiting recovery
LOST = "lost" # 1300 or socket close
def error(self, reqId, code, msg, advancedOrderRejectJson=""):
if code == 1100:
self.conn_state = ConnectionState.DEGRADED
self.pause_new_orders()
elif code == 1101:
self.conn_state = ConnectionState.OK
self.resubscribe_all_market_data()
self.req_open_orders()
elif code == 1102:
self.conn_state = ConnectionState.OK
elif code == 1300:
self.conn_state = ConnectionState.LOST
self.schedule_reconnect()
A separate but related foot-gun: the API requires a nextValidId callback before you submit your first order, and IDs must be strictly increasing across the *session* โ and across all client IDs sharing a TWS instance. Never hardcode order IDs and never reset them mid-session (connection docs). The downstream symptom of getting this wrong is code 103, which we'll come back to in Family 4.
Family 2 โ Market data errors
If you've shipped to production, you've seen these, and you've probably misdiagnosed at least one of them.
| Code | Meaning |
|---|---|
| 354 | Not subscribed to requested market data |
| 10186 | Market data not subscribed; delayed data is also disabled |
| 10090 | Partial market data subscription (some required feeds missing) |
| 2103 | Market data farm disconnected (ISP-side) |
| 2104 | Market data farm connection OK (informational) |
| 2105 | Historical data farm disconnected |
| 2106 | Historical data farm connected (informational) |
| 2107 | Historical data inactive โ available on demand |
| 2108 | Market data farm inactive โ available on demand |
The real errors are 354 / 10186 / 10090. The provoked case at 16:44:22 โ reqMktData for ES with the wrong subscription bundle โ returned 10090 (partial subscription), not 354, because the account had US equities live data but no CME futures bundle. The dispatcher's correct reaction was reqMarketDataType(3) to fall back to delayed quotes rather than aborting the strategy. That distinction (10090 vs 354) is the one that gets misread most often: 354 means "you have *nothing* for this contract"; 10090 means "you have *something* but not what this request needs."
A note on the reqMarketDataType argument, because the values get confused all the time: the IBKR enum is 1 = Realtime, 2 = Frozen, 3 = Delayed, 4 = Delayed-Frozen (market data types reference). 3 gets you delayed live data; 4 gets you delayed data with the last-known frozen value held when the market closes. For a paper bot that just wants to keep ticking through a futures contract it isn't subscribed to, 3 is the right fallback โ 4 is for after-hours diagnostic work.
If your bot trades a fixed universe of tickers on startup, sweep them through reqMktData once at boot and triage anything that comes back 354 / 10090 / 10186 โ never let a missing subscription show up mid-strategy. For the deeper diagnostic walkthrough, see IBKR TWS API: 'Market Data Not Subscribed' โ How to Fix Every Cause.
A subtle one: 2107 ("historical data inactive โ available on demand") looks scary but typically means the farm is in standby and your *next* historical request will wake it up. Don't flush historical caches on this code โ IBKR's own message-codes page documents it as a non-error notification, but it gets misread constantly because the string contains the word "inactive."
Family 3 โ Pacing violations
Pacing is the most consistent way IBKR will quietly throttle a bot, and there are two distinct codes worth keeping straight in your head:
- 162 โ "Historical Market Data Service error message: ..." โ fires when you trip the historical-data limits below.
- 100 โ "Maximum 50 messages/second exceeded" โ fires when you flood the order-message channel.
The historical data limits, per the historical limitations docs, are:
Like what you're reading? Try it yourself โ this link supports ChartedTrader at no cost to you.
Open an IBKR account (referral, up to $1,000 IBKR stock) โ- 15-second rule: no identical historical request within 15 s.
- 2-second / 6-request rule: no more than 6 historical requests for the same
(Contract, Exchange, TickType)within 2 s. - 10-minute rule: no more than 60 historical requests in any 10-minute window.
- BID_ASK requests count double against these limits.
- For bars โค30 s, a violation can trigger throttling and even an API disconnect.
The simplest correct pattern is a token bucket per resource: one for historical-data requests, one for order-message volume, one for market-data ticker count. You don't need a fancy library โ asyncio.Semaphore or a tiny custom counter is enough:
class HistoricalPacer:
def __init__(self):
self._timestamps: deque[float] = deque()
self._lock = asyncio.Lock()
async def acquire(self):
async with self._lock:
now = time.monotonic()
while self._timestamps and now - self._timestamps[0] > 600:
self._timestamps.popleft()
if len(self._timestamps) >= 60:
wait = 600 - (now - self._timestamps[0]) + 0.5
await asyncio.sleep(wait)
return await self.acquire()
self._timestamps.append(now)
Two things this gets right that naive backoff does not: (a) it pre-empts the violation rather than reacting to error code 162, and (b) it survives a long session without unbounded memory growth. Reactive backoff after a pacing error is the classic anti-pattern that shows up over and over in public bug reports โ by the time you see 162, you have usually already been disconnected, and the equivalent in the order channel (a stream of 100s) means orders that never made it to the exchange.
If you trip pacing despite this, treat it the same as 1100: stop sending, wait, *cool down deliberately for at least 10 minutes*, then resume. IBKR's enforcement is sticky; immediate retry is what turns a one-minute timeout into a half-hour blackout.
Family 4 โ Order rejection codes
These come back tied to the order ID you submitted, so route them through your order state machine, not your global error handler.
| Code | Meaning | Recoverable? |
|---|---|---|
| 100 | 50 messages/sec exceeded | Yes โ back off, retry once |
| 101 | Max ticker subscriptions reached | No โ drop a subscription first |
| 102 | Duplicate ticker ID | No โ bug in your ID generator |
| 103 | Duplicate order ID | No โ bug in your ID generator |
| 104 | Cannot modify a filled order | Skip โ order already done |
| 110 | Price doesn't conform to minimum tick variation | Yes โ round to tick and resubmit |
| 200 | No security definition for the request | No โ bad contract spec |
| 201 | Order rejected (catch-all) | Depends โ read errorString |
| 202 | Order cancelled (catch-all) | Depends โ read errorString |
nextValidId the API hands you on reconnect. You can find this exact bug โ same shape, different reporters โ many times in the closed-issue log of erdewit/ib_insync (archived March 2024 but still the most widely deployed Python wrapper). The fix is to *always* take the max of (your_persisted_counter, nextValidId) on every connection, never just one or the other.
110 ("Price doesn't conform to minimum price variation") is silent and brutal โ your strategy computes a price like 127.434, IB's tick size for that contract is 0.01, the order vanishes. In the 2026-05-02 test the AAPL 178.4567 order produced a 110 and no orderStatus callback โ meaning code that waits for orderStatus.Submitted before logging would never even know the order existed. The dispatcher's reaction was to round to the cached minTick and resubmit, which got an immediate fill confirmation 280 ms later. Defensive code looks like:
def round_to_tick(price: float, tick_size: float) -> float:
return round(round(price / tick_size) * tick_size, 8)
Pull minTick for each contract via reqContractDetails once and cache it. Don't trust the symbol's "expected" tick โ futures and options can have asymmetric tick rules around price thresholds, which is the documented reason IBKR uses a per-contract minTick field rather than a per-symbol one.
For the 200 case (provoked at 16:43:05 by querying contract FAKEZ), the response time was sub-second and the errorString literally said No security definition has been found for the request. There is no automated recovery for this โ the contract doesn't exist; drop it from the universe and move on. The mistake to avoid is retrying 200s on a timer, which a few public bots do and which then masks misspelled symbols in production for weeks.
For everything in the 200-series, the errorString is your only friend. IBKR overloads code 201 / 202 across dozens of distinct rejections (margin, short-sale restriction, exchange not open, IOC unfilled, etc). Log the full string, then build a small string-match table over time for the rejections you actually see โ there is no enum that covers them.
Putting it together: the connection lifecycle
If you're using the high-level ib_insync library, you get a slightly different API surface โ errors come through both the errorEvent signal and as RequestError exceptions raised on awaited calls when RaiseRequestErrors=True (ib_insync API docs). Most of the same codes show up; the wrapper just spares you the dispatch boilerplate.
Either way, the production-shape loop has roughly this skeleton:
async def run():
while True:
try:
await ib.connectAsync("127.0.0.1", 7497, clientId=42)
ib.errorEvent += on_error
ib.disconnectedEvent += on_disconnect
await trade_until_close(ib)
except ConnectionRefusedError:
await asyncio.sleep(5)
except asyncio.CancelledError:
break
except Exception:
log.exception("unexpected; reconnecting in 30s")
await asyncio.sleep(30)
finally:
if ib.isConnected():
ib.disconnect()
The shape that matters: connection retry on socket-level failures, never let a single error code take down the whole loop, and isolate strategy logic from connection logic. If your strategy code calls reqMktData directly, a 354 error will splatter through your decision tree. Wrap subscriptions in a thin layer that returns a Result-style object, and let strategies pattern-match on it.
A few patterns that pay rent
1. Two-tier logging. Codes 2103โ2108 and 1102 go to aconnection logger at INFO. Real failures (354, 110, 103, 100, 162, 1100, 1300) go to a trading logger at WARN/ERROR. This matters when you're triaging a 4am alert โ you want the signal, not the heartbeat.
2. Idempotent order submission. Tag every logical order with your own client-side UUID and persist the mapping (your_uuid -> ib_order_id) *before* calling placeOrder. After any reconnect (1101 in particular โ exactly the case the 16:48:51 recovery in the test session walks through), reconcile open orders from the API against your map before resubmitting anything. This pattern shows up in nearly every mature open-source IB bot for one reason: it is the single most important safeguard against doubling your position when a connection blip hits between submit and ack.
3. Authentication side-channel. A surprising number of "connection lost" incidents in the public threads trace back to TWS / IB Gateway losing its session because IB Key 2FA rolled. If you see correlated 1100s at the same time of day, the cause is upstream of your code โ see Interactive Brokers IB Key 2FA Not Working? How to Fix Every Common Issue for the side-channel symptoms.
4. Market-data preflight. Before you start any strategy, call reqMktData for every symbol in your universe and verify a tick comes back within 5 s. Anything that returns 354 / 10090 / 10186 gets logged and either dropped, fallback-routed (to delayed via reqMarketDataType(3)), or escalated. This is cheaper than discovering at 09:30:01 ET that one of your tickers has lost its feed entitlement.
Where this fits next to the official docs
IBKR's TWS API documentation is comprehensive but organized by *capability* (orders, market data, contracts), not by *failure mode*. The codes table is alphabetical and lists hundreds of values, most of which a normal Python bot will never see. The codes covered here โ roughly two dozen โ are the ones that account for almost all real-world dispatch logic in production accounts I've reviewed and in the public open-source IB wrappers.
If you want one mental model to take away: the IB error callback is not an error stream, it is a control-plane stream. Treat it that way and the dispatch falls out naturally. Treat it as "errors to log" and you will spend a year discovering the same bugs everyone before you discovered.
Want to actually try this on a paper account?
The dispatcher patterns above are written against a real paper login because that's the only way to see codes like 110 and 100 fire in proper wall-clock sequence โ neither shows up reliably in mocked tests. If you don't already have an IBKR account, you can open one through supa.is's referral link (affiliate disclosure at the top of this article โ supa.is may earn a commission if you sign up through that link, at no cost to you). A paper login is free and gets you the same TWS API surface as a funded account.
Once you're in, the fastest way to validate this article's claims for yourself is the five-minute provocation script: round-trip an AAPL LMT at a sub-tick price (force 110), reqContractDetails on a fake symbol (force 200), reqMktData on a contract you don't have entitlements for (force 354 or 10090), and queue a tight burst of LMT orders to trip 100. If your dispatcher reacts to all four without manual intervention, your error-handling layer is in production shape.