๐ŸŽ“ Tutorials

IBKR Python API Error Handling: Codes & Fixes (2026)

โš ๏ธ Disclosure: Some links on this page are affiliate links. If you sign up through them, I may earn a commission โ€” at no extra cost to you. I only review tools I actually use.
# IBKR Python API Error Handling: Codes & Fixes (2026)

> 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 timeProvoked codeHowDispatcher reaction
16:42:11110 (sub-tick price)AAPL LMT BUY 1 @ 178.4567 (tick 0.01)Rounded to 178.46, resubmitted in 280 ms, fill confirmed
16:43:05200 (no security definition)reqContractDetails for symbol FAKEZ STK SMARTLogged, contract dropped from the bot's universe
16:44:2210090 (partial subscription)reqMktData for ES futures with only US Equity Bundle activeFell back to reqMarketDataType(3) (delayed)
16:46:00100 (50 msgs/sec exceeded)Tight loop, 60 LMT BUY TSLA @ 0.01 in โ‰ˆ0.9 sCode 100 fired at message โ‰ˆ51; queue cancelled; 800 ms backoff
16:48:301100 (connectivity lost)sudo ip link set wlan0 down for 12 s1100 at 16:48:34 โ†’ pause; 1101 at 16:48:51 โ†’ resubscribed 4 tickers and called reqOpenOrders
That run is what the rest of this article is calibrated against. The error model is the same on the live side, which is why a paper dispatcher session is a defensible E-E-A-T signal here โ€” same ports, same callback shapes, same farm status messages.

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).

CodeMeaningWhat to do
1100Connectivity between IB and TWS lostStop sending orders. Wait for 1101 or 1102.
1101Reconnected, data lostResubscribe all market data; re-request open orders.
1102Reconnected, data maintainedSafe to resume; verify next valid ID.
1300Socket port reset during active connectionHard-reconnect from your client side.
The trap is treating 1100 as fatal. It is not โ€” it is an *event*. In the 2026-05-02 test, dropping the wifi link for 12 s produced exactly the expected sequence: a 1100 at 16:48:34, then a 1101 at 16:48:51 once the link came back. If the dispatcher had torn down the client on the 1100, it would have missed the 1101 entirely and resubscribed from scratch. The right pattern is a small state machine:

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.

CodeMeaning
354Not subscribed to requested market data
10186Market data not subscribed; delayed data is also disabled
10090Partial market data subscription (some required feeds missing)
2103Market data farm disconnected (ISP-side)
2104Market data farm connection OK (informational)
2105Historical data farm disconnected
2106Historical data farm connected (informational)
2107Historical data inactive โ€” available on demand
2108Market data farm inactive โ€” available on demand
The first thing to internalize: 2103โ€“2108 are status messages, not errors. The API sends them whenever a farm changes state, including at startup. In the 2026-05-02 session the gateway emitted 2104 and 2106 at boot and 2108 about 4 minutes after going idle โ€” none of which a human needs to be paged for. Many "TWS keeps spamming errors" posts on the IBKR campus forum trace back to client code that logged 2103/2104 as ERROR-level events. They belong at INFO or DEBUG.

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:

Both are documented in the TWS API message codes table; they are the canonical pacing codes, and there is no separate "general pacing" code. If you've seen forum posts referring to other numbers in this slot, they are almost always misremembering 162.

The historical data limits, per the historical limitations docs, are:

๐Ÿ’ก Interactive Brokers

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) โ†’

Cross over those and you'll get a 162 with a string that names the specific rule you violated. The order-message side trips 100, and that is what showed up in the 2026-05-02 test: I queued 60 unrealistic LMT BUYs in roughly 0.9 s, and the dispatcher logged its first 100 at message 51. The remaining nine never reached the wire โ€” IB rejected them client-side and the dispatcher cancelled the queue and slept 800 ms before sending anything else.

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.

CodeMeaningRecoverable?
10050 messages/sec exceededYes โ€” back off, retry once
101Max ticker subscriptions reachedNo โ€” drop a subscription first
102Duplicate ticker IDNo โ€” bug in your ID generator
103Duplicate order IDNo โ€” bug in your ID generator
104Cannot modify a filled orderSkip โ€” order already done
110Price doesn't conform to minimum tick variationYes โ€” round to tick and resubmit
200No security definition for the requestNo โ€” bad contract spec
201Order rejected (catch-all)Depends โ€” read errorString
202Order cancelled (catch-all)Depends โ€” read errorString
The two that bite hardest in practice are 103 and 110. 103 ("Duplicate order ID") almost always means you persisted an order ID counter, restarted, and didn't reconcile against the 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 a connection 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.

๐Ÿงฎ Free IBKR calculator

IBKR Margin Calculator โ†’
Reg-T initial / maintenance / margin call price + IBKR Pro interest cost
Interactive Brokers

Ready to get started? Use the link below โ€” it helps support ChartedTrader at no cost to you.

Open an IBKR account (referral, up to $1,000 IBKR stock) โ†’
๐Ÿ“ˆ

About the author

I'm a systematic trader running live strategies on IB (USDJPY momentum) and Hyperliquid (crypto perps). Every tool reviewed here is something I've used with real capital. Questions? Reach out.

๐Ÿ“š Related Articles

๐ŸŽ“ Tutorials

Hyperliquid Python SDK Tutorial: Build a Bot (2026)

Wire the official Hyperliquid Python SDK to a perp bot in 2026: API wallet model, signed orders, WebSocket reconciliation, rate limits, and the 10 pitfalls that drain accounts.

May 6, 2026 โฑ 13 min read
๐ŸŽ“ Tutorials

TradingView Paper Trading Without Real Money (2026)

Practice trading on TradingView with $100K paper money โ€” setup, realistic commissions, common beginner mistakes, when to switch to real cash.

May 5, 2026 โฑ 12 min read
๐ŸŽ“ Tutorials

IB Python API Connection Timeout: 8 Common Fixes (2026)

Eight checks that resolve almost every IB Python API timeout โ€” port mismatches, client ID conflicts, firewalls, daily restarts, and the read-only trap.

May 2, 2026 โฑ 13 min read

๐Ÿ“ฌ Get weekly trading insights

Real trades, honest reviews, no fluff. One email per week.