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, None for failed or ambiguous lookups; ib_insync left failed contracts out. IBDefaults can replace IBKR’s placeholders (-1 for empty prices, 0 for empty sizes, nan for 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 eventkit became aeventkit.
  • 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

  1. Inventory: find every ib_insync import and note the events, util.startLoop(), bracket orders, historical data, account values and logging you use.
  2. Rebuild instead of upgrading in place: record Python, Gateway and package versions, then install ib_async==2.1.0 into a new virtual environment.
  3. 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).
  4. Fix contract handling: every caller of qualifyContracts() must handle None (code below).
  5. Update logging: logger names changed from ib_insync.* to ib_async.* (such as ib_async.wrapper), so old filters silently stop applying.
  6. Decide on placeholders: keep -1, 0 and nan or switch to None via IBDefaults; the choice affects every comparison in strategy and risk code.
  7. 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, exchange and currency; ib_async 2.x then discards matches of other security types. Add primaryExchange or multiplier where needed.
  • Store the conId after qualifying and reuse it.
  • In option chains from reqSecDefOptParams(), some expiry and strike combinations do not exist; filter the None results 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, 2026ib_asyncOfficial TWS API with sync wrapper
MaintainerCommunity organization ib-api-reloaded (Matt Stancliff), not affiliated with IBKRInteractive Brokers
Current version2.1.0 (PyPI, December 8, 2025); 3.0 in developmentStable 1050.02 (September 9, 2026), Latest 1051.01 (September 29, 2026); the Python API ships only in Latest
Python3.10 or newer; PyPI lists up to 3.14, but see issue #2003.11.0 or newer
Installationpip install ib_asyncIBKR’s TWS API download page
ProtocolOwn implementation of IBKR’s classic socket protocol (API server version up to 178); protobuf only in 3.0Socket protocol with Protocol Buffers
LicenseBSD-2-ClauseGPL from 10.49
IBKR supportNo coding help; IBKR can review API logsIBKR API Support
Effort from ib_insyncLow: same classes, methods and eventsHigh: 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: None versus -1, Decimal versus float

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

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.