ib_async vs ib_insync: Migrating an IBKR Python Bot in 2026
ib_async is the maintained continuation of ib_insync, and for an existing Interactive Brokers bot it is usually the shortest migration path: ib_insync has had no release since 0.9.86 in July 2023. Most code only needs new imports, but since ib_async 2.0, qualifyContracts() returns None for unresolved contracts, so old empty-list checks stop catching failures. As of November 3, 2026, the current release is 2.1.0 for Python 3.10 or newer, with 3.0 in development. The alternative, IBKR’s own TWS API with its synchronous Python wrapper, means a larger rewrite and a GPL license from version 10.49.
Note: This article describes the legal situation in general terms as of the date below. It is not legal or tax advice and does not replace a review of your specific case by a lawyer or tax advisor. All information without guarantee of completeness or timeliness.
Is ib_insync still maintained in 2026?
No. The last PyPI release is 0.9.86 from July 2, 2023. Its author, Ewald de Wit, died in March 2024; according to the ib_async README, the project continued under that name because its maintainers had lost access to the original repositories and packaging.
Old scripts may still connect: ib_insync speaks IBKR’s socket protocol itself (up to API server version 176) and works as long as TWS or IB Gateway accepts that range. IBKR’s requirements, however, list the current Stable or Latest release of TWS or IB Gateway. IBKR’s documentation says ib_insync is built on a legacy API release and no longer updated, and calls ib_async more recent, “though still legacy”. IBKR gives no coding help for either package and advises its direct TWS API whenever possible.
To have the migration and the parity test done for you, see Python bot development for Interactive Brokers with ib_async. If your Gateway still starts through IBC, also read running IB Gateway headless in Docker after IBC.
ib_async vs ib_insync: what changed in 2.x and what 3.0 brings
ib_async started as a rename: per its changelog, 1.0.0 (March 2024) was functionally equivalent to ib_insync 0.9.86, with minimum Python raised from 3.6 to 3.10. The changes that matter came later:
- 1.0.2 (June 2024):
connect(fetchFields=…)can skip parts of the startup sync. - 2.0.0 (June 2025):
qualifyContracts()returns one result per input contract,Nonefor failed or ambiguous lookups; ib_insync left failed contracts out.IBDefaultscan replace IBKR’s placeholders (-1for empty prices,0for empty sizes,nanfor unset values). Validation warnings no longer remove orders from the local cache, which could show an order as canceled while it was still live. - 2.0.1: the dependency
eventkitbecameaeventkit. - 2.1.0 (December 2025): more tick types and ticker fields, explicit
tzdata, event-loop fixes.
2.1.0 is still the newest PyPI release. The next-protobuf branch declares version 3.0.0, requires Python 3.11 or newer and uses IBKR’s protobuf protocol. Its draft migration guide lists three breaking areas: Decimal or None instead of float for prices, sizes and commissions, commission renamed to commissionAndFees, and dataclasses without index access instead of named tuples.
That deadline is the ib_async project’s statement, not a confirmed IBKR rule: according to the README on that branch, IBKR will require TWS or IB Gateway 10.51 or later for API connections in December 2026, and only 3.0 will be supported afterward. As of November 3, 2026, neither IBKR’s TWS API changelog nor its 2026 API release notes contained such a notice; the last minimum-version increase announced in that changelog was to 10.30 from March 20, 2025. IBKR’s download page recommends TWS or IB Gateway 1050.02 or higher “for comprehensive feature support”. Pin ib_async==2.1.0 now and plan 3.0 as a second, separately tested step.
ib_insync to ib_async migration checklist
- Inventory: find every
ib_insyncimport and note the events,util.startLoop(), bracket orders, historical data, account values and logging you use. - Rebuild instead of upgrading in place: record Python, Gateway and package versions, then install
ib_async==2.1.0into a new virtual environment. - Switch imports and set host, port and client ID explicitly.
connect()defaults to port 7497, the TWS paper port in IBKR’s defaults; TWS live uses 7496, IB Gateway 4001 (live) and 4002 (paper). - Fix contract handling: every caller of
qualifyContracts()must handleNone(code below). - Update logging: logger names changed from
ib_insync.*toib_async.*(such asib_async.wrapper), so old filters silently stop applying. - Decide on placeholders: keep
-1,0andnanor switch toNoneviaIBDefaults; the choice affects every comparison in strategy and risk code. - Run the parity test in the paper account before any live order.
Migration code: qualifyContracts and reconnects in ib_async
Two ib_insync patterns break quietly. if not ib.qualifyContracts(c) used to catch an unknown contract, and ib.qualifyContracts(c)[0] raised an IndexError. In ib_async 2.x, the first check stays silent, because [None] is a non-empty list, and the second returns None, which fails later at the order request. Only qualifyContractsAsync() takes returnAll=True to return the candidates for an ambiguous contract.
Reconnects should keep one fixed client ID. IBKR documents that connecting with a client ID still in use fails like a broken socket, and that an API order can be modified from a session with the same username and client ID that placed it, so retry with backoff instead of switching IDs. Call the helper from your main loop when ib.isConnected() is False, not from an event handler: blocking ib_async calls run the asyncio loop themselves. Each connect reloads positions, open orders and executions by default; compare them with your bot’s state before trading resumes, and never resubmit orders automatically.
Test condition: both files ran on November 3, 2026 with Python 3.12, standard library only, against a stubbed IB object that mimics the ib_async 2.1.0 return format; no broker connection, no orders. It checks the None handling, the backoff and that every retry uses the same client ID; it verifies the logic, not ib_async itself.
# migrate.py: contract and reconnect logic for ib_async 2.x
import asyncio
# BEFORE, ib_insync 0.9.86 (failed contracts were left out):
# from ib_insync import IB, Stock
# if not ib.qualifyContracts(contract): ... # [] meant "not found"
# AFTER, ib_async 2.x (one slot per input, None if unknown or ambiguous):
# from ib_async import IB, Stock
# valid, failed = split_qualified(contracts, ib.qualifyContracts(*contracts))
def split_qualified(requested, results):
"""Return (valid, failed) from ib_async 2.x qualifyContracts results."""
if len(results) != len(requested):
raise ValueError("expected one result per contract (ib_async 2.x)")
valid, failed = [], []
for req, res in zip(requested, results):
# a list holds candidates from qualifyContractsAsync(..., returnAll=True)
if res is None or isinstance(res, list):
failed.append(req)
else:
valid.append(res)
return valid, failed
def connect_with_backoff(ib, host, port, client_id, attempts=6, wait=2.0):
"""Reconnect with the same fixed client ID. Never re-sends orders."""
for attempt in range(1, attempts + 1):
try:
ib.connect(host, port, clientId=client_id, timeout=10)
return attempt
except (OSError, asyncio.TimeoutError) as exc: # refused, ID in use
if attempt == attempts:
raise ConnectionError(f"gave up after {attempts} attempts") from exc
ib.sleep(wait) # ib.sleep, not time.sleep, in ib_async code
wait = min(wait * 2, 60.0)# test_migrate.py: run with python3 test_migrate.py (standard library only)
from dataclasses import dataclass
from migrate import split_qualified, connect_with_backoff
@dataclass
class Stock: # stand-in for ib_async.Stock
symbol: str
class StubIB:
"""Stands in for ib_async.IB: no socket, no broker, no orders."""
def __init__(self, refusals):
self.refusals, self.client_ids, self.waits = refusals, [], []
def connect(self, host, port, clientId, timeout):
self.client_ids.append(clientId)
if len(self.client_ids) <= self.refusals:
raise ConnectionRefusedError(61, "Connect call failed")
def sleep(self, secs):
self.waits.append(secs)
def qualifyContracts(self, *contracts): # ib_async 2.x return format
return [c if c.symbol == "AAPL" else None for c in contracts]
ib = StubIB(refusals=2)
assert connect_with_backoff(ib, "127.0.0.1", 4002, client_id=21) == 3
assert ib.client_ids == [21, 21, 21] and ib.waits == [2.0, 4.0]
req = [Stock("AAPL"), Stock("NOSUCHSYM")]
res = ib.qualifyContracts(*req)
assert res == [req[0], None] and bool(res) # "if not res" misses the failure
valid, failed = split_qualified(req, res)
assert valid == [req[0]] and failed == [req[1]]
assert split_qualified(req[:1], [[Stock("AAPL"), Stock("AAPL")]])[1] == req[:1]
checks = [(ValueError, lambda: split_qualified(req, res[:1])), # ib_insync-style
(ConnectionError, lambda: connect_with_backoff(
StubIB(9), "127.0.0.1", 4002, 21, attempts=3))]
for expected, call in checks:
try:
call()
except expected as exc:
assert type(exc) is expected # not the raw ConnectionRefusedError
print("raised as expected:", exc)
else:
raise AssertionError(f"expected {expected.__name__}")
print("all checks passed")
ib_async error 200: no security definition found
It is usually TWS API error code 200, logged as Error 200, reqId …: No security definition has been found for the request. IBKR lists two meanings: the contract matches nothing in its database, usually because a parameter is wrong or missing, or the description is ambiguous. For stocks sharing symbol, currency and exchange, IBKR asks for the primary exchange; for futures with several multipliers per expiry, for the multiplier.
By default (RaiseRequestErrors = False), ib_async logs Unknown contract or Ambiguous contract, and qualifyContracts() returns None in that slot. With ib.RaiseRequestErrors = True, it raises a RequestError carrying the code. Typical fixes:
- Always set
secType,exchangeandcurrency; ib_async 2.x then discards matches of other security types. AddprimaryExchangeormultiplierwhere needed. - Store the
conIdafter qualifying and reuse it. - In option chains from
reqSecDefOptParams(), some expiry and strike combinations do not exist; filter theNoneresults instead of treating them as failures.
The phrase also matches GitHub issue #200: on Python 3.14, util.startLoop() (used in notebooks) followed by a blocking call fails with RuntimeError('Timeout should be used inside a task'), caused by the nest_asyncio dependency. The maintainer will not patch 2.x and marked it fixed in the 3.0 line; the thread documents a nest_asyncio2 workaround. Until 3.0 ships, use Python 3.10 to 3.13.
ib_async or the official TWS API with TWSSyncWrapper?
IBKR’s classic Python API is callback-based: requests go out through EClient, answers arrive later in EWrapper. Since TWS API 10.40, TWSSyncWrapper (Python only) combines both, runs the message loop in its own thread and returns values directly, with timeouts of 30 seconds by default, 5 for contract details and 3 for open orders.
| Status as of November 3, 2026 | ib_async | Official TWS API with sync wrapper |
|---|---|---|
| Maintainer | Community organization ib-api-reloaded (Matt Stancliff), not affiliated with IBKR | Interactive Brokers |
| Current version | 2.1.0 (PyPI, December 8, 2025); 3.0 in development | Stable 1050.02 (September 9, 2026), Latest 1051.01 (September 29, 2026); the Python API ships only in Latest |
| Python | 3.10 or newer; PyPI lists up to 3.14, but see issue #200 | 3.11.0 or newer |
| Installation | pip install ib_async | IBKR’s TWS API download page |
| Protocol | Own implementation of IBKR’s classic socket protocol (API server version up to 178); protobuf only in 3.0 | Socket protocol with Protocol Buffers |
| License | BSD-2-Clause | GPL from 10.49 |
| IBKR support | No coding help; IBKR can review API logs | IBKR API Support |
| Effort from ib_insync | Low: same classes, methods and events | High: calls and return types change |
Rule of thumb: ib_async fits an existing event-driven code base that needs maintained code quickly; the official API fits new, mostly request-response projects that value vendor proximity. There, calls change rather than imports: qualifyContracts() becomes get_contract_details(), placeOrder() becomes place_order_sync(), openTrades() becomes get_open_orders(), and results are lists and dictionaries instead of Trade objects. Without TWS or Gateway, compare the IBKR Web API vs the TWS API first.
Licensing: ib_async is BSD-2-Clause. Per IBKR’s changelog of August 3, 2026, TWS API 10.49 and later is released under the GNU General Public License, without a version number, so the license file in your download counts. GPL obligations generally concern distribution: if you ship software to clients or partners, have a software-licensing lawyer check it (general information, not legal advice). ib_async 3.0 builds its protobuf bindings from IBKR’s TWS API 1051.01 archive, so check its terms again when it ships.
Parity test in the paper account before going live
Neither path is risk-free. ib_async is a volunteer project with one listed maintainer, and the README on its 3.0 branch lists 2.1.0 as unsupported after December 2026. For place_order_sync(), IBKR states that the timeout only concerns the response and the order is submitted regardless, so an automatic retry after a timeout can duplicate an order; check get_open_orders() first.
A parity test checks that new code does what old code did. Run both against the same paper account, each with its own fixed client ID, and compare automatically:
- contracts: same
conId, exchange and trading class - historical data: identical bars for identical periods and settings
- account: positions and account values at the same moment
- order life cycle: statuses, partial fills, cancels, rejections, reconnects
- types:
Noneversus-1,Decimalversusfloat
IBKR simulates paper fills from the top of the book only, always simulates stops and complex order types, and offers no VWAP, Auction, RFQ or Pegged-to-Market orders there. Parity shows that old and new code agree, not how orders fill live. Automated trading, especially with leverage, can lead to the total loss of the capital used.
What to do now
- List where ib_insync, IBC and old Gateway builds still run, with versions.
- Choose a path: ib_async 2.1.0 pinned on Python 3.10 to 3.13, a planned rewrite on the sync wrapper, or both with separate client IDs.
- Put a thin adapter between library objects and your own data model, without index access or float equality; it makes parity tests and the move to 3.0 cheaper.
- Watch ib_async releases and IBKR’s changelog; test every update in the paper account first.
- Handing the work over? See hiring an Interactive Brokers API developer.
Frequently asked questions
Is ib_async a drop-in replacement for ib_insync?
Almost. ib_async 1.0.0 was functionally equivalent to ib_insync 0.9.86, so for most code the first step is replacing the import. Later releases changed some behavior: since 2.0.0, qualifyContracts returns None for contracts that could not be qualified instead of leaving them out, logger names start with ib_async, and the event library comes from the aeventkit package. Use a fresh virtual environment and run a parity test before trading live.
Is ib_insync still maintained?
No. The last release on PyPI is 0.9.86 from July 2023, and Interactive Brokers documents that ib_insync is built on a legacy TWS API release and no longer updated. Existing scripts may keep connecting as long as TWS or IB Gateway accepts the protocol versions the library negotiates, but bug fixes and support for new API features will not arrive. The maintained continuation is ib_async.
What does error 200 mean in ib_async?
Error 200 is an Interactive Brokers TWS API code, not an ib_async bug. It means no security definition was found for the contract, usually because a field is wrong or missing, or that the definition is ambiguous. Add secType, exchange, currency and, where needed, primaryExchange or multiplier, or use the stored conId. In ib_async 2.x, qualifyContracts then returns None for that contract unless RaiseRequestErrors is enabled.
How do I reconnect ib_async after IB Gateway restarts?
Check ib.isConnected() in your main loop and, when it returns False, call connect again with the same client ID and a growing wait between attempts, using ib.sleep instead of time.sleep. An attempt can fail while the old session still holds the client ID, so keep retrying rather than switching IDs. After reconnecting, ib_async reloads positions, open orders and executions by default; compare them with your bot’s state before it trades again, and never resubmit orders automatically.
Does ib_async work with Python 3.14?
Not reliably in version 2.1.0. GitHub issue 200 reports that util.startLoop, used in Jupyter notebooks, followed by a synchronous call fails on Python 3.14 because of the nest_asyncio dependency. The maintainer said 2.x will not be patched and marked the issue fixed in the 3.0 line, which was not on PyPI as of November 3, 2026. Python 3.10 to 3.13 is the safer choice for now.
Does ib_async need TWS or IB Gateway?
Yes. ib_async is a client for the socket API of TWS and IB Gateway, so one of them must be running with API access enabled and reachable on the configured port. It does not need IBKR’s ibapi package because it implements the protocol itself. If you do not want to run TWS or IB Gateway at all, compare IBKR’s Web API, which is a separate interface.
Should a trading bot use ib_async or the official TWS API?
For an existing ib_insync code base, ib_async is usually the shorter path because classes, methods and events stay the same. The official TWS API with TWSSyncWrapper, available for Python since version 10.40, suits new projects and request-response systems and comes directly from Interactive Brokers, but it means rewriting calls, Python 3.11 or newer and the GPL license that applies from version 10.49.
Does the GPL license affect a private trading bot?
GPL obligations are generally tied to distributing software, so scripts that you only run yourself are usually less affected than products you sell or hand to clients. The IBKR changelog does not name a GPL version, so the license text in your download counts. This is general information, not legal advice; if you distribute software built on TWS API 10.49 or later, have the details checked by a lawyer.
Sources and status
- ib_insync and ib_async (Third-Party API Platforms) — Interactive Brokers, TWS API documentation (retrieved November 3, 2026)
- Synchronous API Wrapper: Introduction — Interactive Brokers, TWS API documentation (retrieved November 3, 2026)
- place_order_sync() — Interactive Brokers, TWS API documentation (retrieved November 3, 2026)
- connect_and_start(): default ports — Interactive Brokers, TWS API documentation (retrieved November 3, 2026)
- TWS API changelog (TWS API 10.49 under the GPL, August 3, 2026; minimum TWS and IB Gateway 10.30 from March 20, 2025; newest entry October 30, 2026, no notice on TWS 10.51) — Interactive Brokers (retrieved November 3, 2026)
- Trader Workstation (TWS) 2026 API Production Release Notes (10.49 to 10.52, no minimum-version notice) — Interactive Brokers, IBKR Guides (retrieved November 3, 2026)
- Broken API socket connection (client ID already in use) — Interactive Brokers, TWS API documentation (retrieved November 3, 2026)
- TWS API download (Stable 1050.02, Latest 1051.01, recommended TWS or IB Gateway version) — Interactive Brokers (retrieved November 3, 2026)
- TWS API requirements (TWS or IB Gateway release, minimum Python version) — Interactive Brokers, TWS API documentation (retrieved November 3, 2026)
- TWS API error codes — Interactive Brokers, TWS API documentation (retrieved November 3, 2026)
- Modifying Orders — Interactive Brokers, TWS API documentation (retrieved November 3, 2026)
- About Paper Trading Accounts — Interactive Brokers, IBKR Guides (retrieved November 3, 2026)
- ib_async repository (README, license, history) — ib-api-reloaded on GitHub (retrieved November 3, 2026)
- ib_async 2.1.0 source code: ib.py (qualifyContracts, connect, startup sync, sleep) — ib-api-reloaded on GitHub (retrieved November 3, 2026)
- ib_async changelog — ib-api-reloaded (retrieved November 3, 2026)
- ib_async next-protobuf branch (3.0.0 in development, README on TWS 10.51 and support after December 2026, draft v3 migration guide) — ib-api-reloaded on GitHub (retrieved November 3, 2026)
- Issue #200: ib_async does not work with py3.14 — ib-api-reloaded on GitHub (retrieved November 3, 2026)
- ib-async on PyPI (2.1.0) — Python Package Index (retrieved November 3, 2026)
- ib-insync on PyPI (0.9.86) — Python Package Index (retrieved November 3, 2026)
As of November 2026. Third-party rules, prices and APIs may have changed since; the providers' current documentation and the official texts are authoritative.
This article is also available in German: ib_insync-Alternative: ib_async, Sync-Wrapper und GPL.
Need an ib_insync bot migrated to ib_async or reviewed? Get in touch — start with an inventory of your code, a clear migration path and a paper-account parity test before anything goes live. More: Trading bot development for Interactive Brokers and MetaTrader 5.