Short answer: ib_insync vs ib_async: ib_async is the maintained continuation and ib_insync is frozen at 0.9.86 from July 2023, so new work and live bots should plan to move. A static comparison run on 5 November 2026 found no public name removed and only IB.connect, IB.connectAsync and IB.qualifyContractsAsync changed in signature, but 69 of 420 function bodies differ, and tests on a synthetic socket showed different results for unknown contracts, order errors and state after disconnect. Both are third-party libraries, not affiliated with IBKR.
Most migration notes stop at the import rename. That is enough to make a bot start, and not enough to make it behave the same. This guide publishes the comparison so you can repeat it, then shows where behavior differs, tested on 127.0.0.1 against a synthetic server because no IBKR account was available. A person still has to run the result on a paper account.
These are third-party libraries under the BSD 2-Clause license (PyPI's classifier for ib_async says Other/Proprietary, but its LICENSE file and license field say BSD), and this guide contains no strategy. No broker is recommended and there are no referral links; rules depend on your country and account type. For readers in Turkey, separate rules apply to leveraged FX with foreign firms: the Capital Markets Board said on 23 February 2023 (opens in a new tab) that residents may trade leveraged products only through firms it has authorized (Law No. 6362 (opens in a new tab) Articles 99(4) and 109(2)). Automated trading can lose money. First-time setup is in Interactive Brokers API Python.
ib_insync vs ib_async vs ibapi: status on 5 November 2026
ib_insync is archived, ib_async is active but heading for a large release, and ibapi is the only one IBKR supports directly.
| Item | ib_insync | ib_async | ibapi (official) |
|---|---|---|---|
| Latest release | 0.9.86, 2 July 2023 | 2.1.0, 8 December 2025 | 1052.01, 30 October 2026 |
| Python | 3.6 or newer; classifiers stop at 3.11 | 3.10 or newer; classifiers reach 3.14 | 3.11 or newer |
| Repository | Archived 14 March 2024 | Active; commits on a development branch to 4 November 2026 | Installer download; private beta repository |
| Client version offered | v157..176 | v157..178 | v100..226 |
| Wire format | Text | Text | Protobuf, message by message, from server version 201 |
| Needs ibapi | No | No | It is ibapi |
| Style | Sync and asyncio | Sync and asyncio | Callbacks, or TWSSyncWrapper |
| License | BSD 2-Clause | BSD 2-Clause | GPL-3.0-or-later |
ib_insync's author, Ewald de Wit, died on 11 March 2024, as a family notice in the project's README (opens in a new tab) records. ib_async 1.0.0 followed a week later under new maintainers and described itself (opens in a new tab) as functionally equal to 0.9.86.
IBKR's own note (opens in a new tab) says ib_insync uses a legacy API release and is no longer updated, and tells those who want that structure on supported TWS releases to move to ib_async, which it calls more recent though still legacy. It says this is not an endorsement, offers no coding help for these packages, can review your API logs, and advises its direct API whenever possible. Whether a current TWS keeps accepting clients at version 176 or 178 could not be tested here, though the report in issue 243 suggests ib_async 2.1.0 still connected on 4 November 2026.
The API diff: no names removed, 69 function bodies changed
Every public name in ib_insync 0.9.86 still exists in ib_async 2.1.0, so name-level breakage is rare, but that says little about behavior.
The script reads both wheels as text and parses them with ast, without importing or running them. It compares public module functions, classes, methods and their argument names; for bodies it strips docstrings and normalizes the package name. A changed body is not always a behavior change: 4 of the 69 differ only in annotations or in the container used for a membership test. The wheel hashes matched PyPI's published values.
import ast, sys, zipfile
FUNC = (ast.FunctionDef, ast.AsyncFunctionDef)
def scan(wheel, pkg):
names, bodies = {}, {}
z = zipfile.ZipFile(wheel)
for n in z.namelist():
if not (n.startswith(pkg + "/") and n.endswith(".py")):
continue
mod = n[len(pkg) + 1:-3]
for node in ast.parse(z.read(n).decode()).body:
if getattr(node, "name", "_").startswith("_"):
continue
funcs = [(node.name, node)] if isinstance(node, FUNC) else []
if isinstance(node, ast.ClassDef):
names[f"{mod}.{node.name}"] = None
funcs = [(f"{node.name}.{m.name}", m) for m in node.body
if isinstance(m, FUNC) and not m.name.startswith("_")]
for name, fn in funcs:
names[f"{mod}.{name}"] = [a.arg for a in fn.args.args + fn.args.kwonlyargs]
body = fn.body[1:] if ast.get_docstring(fn) else fn.body
bodies[f"{mod}.{name}"] = "".join(map(ast.dump, body)).replace(pkg, "PKG")
return names, bodies
old, old_b = scan(sys.argv[1], "ib_insync")
new, new_b = scan(sys.argv[2], "ib_async")
common = old_b.keys() & new_b.keys()
print("public names", len(old), len(new))
print("removed", len(old.keys() - new.keys()), "added", len(new.keys() - old.keys()))
print("signatures changed", sorted(k for k in old.keys() & new.keys() if not old[k] == new[k]))
print("function bodies", len(common), "changed", sum(not old_b[k] == new_b[k] for k in common))$ python api_diff.py ib_insync-0.9.86-py3-none-any.whl ib_async-2.1.0-py3-none-any.whl
public names 527 540
removed 0 added 13
signatures changed ['ib.IB.connect', 'ib.IB.connectAsync', 'ib.IB.qualifyContractsAsync']
function bodies 420 changed 69All additions are optional: connect gains raiseSyncErrors and fetchFields, qualifyContractsAsync gains keyword-only returnAll, and IB() gains defaults, which the script skips as a private name. Added names include Trade.isWaiting, Trade.isWorking, OrderStateNumeric, IBDefaults and StartupFetch.
Behavior differences on a synthetic socket
The same probe, run against the same local mock, gave different results in the places a live bot is most likely to depend on.
| Probe | ib_insync 0.9.86 | ib_async 2.1.0 |
|---|---|---|
| qualifyContracts on an unknown contract | [] | [None] |
| [c] = ib.qualifyContracts(...) | ValueError at that line | c is None; AttributeError later in placeOrder |
| Error 202 on a working order | Stays Submitted, active | Cancelled, done |
| Error 321 on a working order | Cancelled, done | ValidationError, active |
| Error 110 on a working order | Stays Submitted, active | ValidationError, active |
| Error 439 on a working order | Cancelled, done | Cancelled, done |
| disconnect() returns | None | A status string |
| ib.trades() after disconnect, with 4 before | 4 | 0, state is cleared |
| Inactive status | Not a done state | A done state |
| Plain limit order on the wire, both at server version 176 | 115 fields | 115 fields, identical |
Three of these need a decision. A smaller result list becoming a list holding None moves the failure away from the cause; the 2.0.0 changelog (opens in a new tab) marks this as breaking. A new ValidationError status means an order that looks wrong may still be live, and a switch on status with no default branch will miss it. And issue 243 (opens in a new tab), opened on 4 November 2026, reports that a refused modification with error 439 marked a working order Cancelled in 2.1.0 on a real TWS session. That is a user report, and the probe above only confirms that the library treats 439 that way. Read order state back with reqOpenOrders rather than trusting a local Cancelled after an error.
Hypothetical example. A bot counts its resting orders by checking whether status is Submitted or PreSubmitted. After error 110 on a resting order, ib_async reports ValidationError, the count falls to zero and the bot may place another order while the first is still live. Trade.isActive() returned True in that test, so use it instead.
At their own highest versions the libraries send 115 and 114 fields, because ib_async drops faProfile from version 177, so compare wire logs only at the same negotiated version.
ib_async error 200: what it means and how to handle it
Error 200 comes from IBKR rather than from the library: its list (opens in a new tab) uses the code when no security definition matches and when a contract description is ambiguous.
Both libraries logged the same lines in the test, apart from the logger name; only the return value changed:
ib_async.wrapper ERROR Error 200, reqId 103: No security definition has been found for the request, contract: Stock(symbol='NOPE', exchange='SMART', currency='USD')
ib_async.ib WARNING Unknown contract: Stock(symbol='NOPE', exchange='SMART', currency='USD')contract = Stock("XYZ", "SMART", "USD") # placeholder symbol
[qualified] = ib.qualifyContracts(contract)
if qualified is None: # ib_async: no match, or ambiguous
raise RuntimeError(f"not qualified: {contract}")
trade = ib.placeOrder(qualified, LimitOrder("BUY", 10, 10.0))To fix the cause, IBKR's list suggests setting the primary exchange when a symbol, currency and exchange are not unique, and the multiplier for futures with several at one expiration. In ib_async, await ib.qualifyContractsAsync(contract, returnAll=True) returns the candidates for an ambiguous request instead of None; the synchronous qualifyContracts has no such argument. An offline test that fed two candidates into the library's own logic returned both with returnAll=True and [None] without it; no real ambiguous reply was involved.
Migration checklist for a live ib_insync bot
Treat this as a code change with tests, not a rename, and keep a way back.
- Pin both. Keep ib_insync==0.9.86 in a rollback environment and move to ib_async==2.1.0 in a new one on Python 3.10 or newer. The install pulled aeventkit instead of eventkit, and tzdata below 2026.0, which can conflict with other pins. PyPI lists Python 3.14, but issue 200 (opens in a new tab) reports util.startLoop() failing there, marked as fixed for 3.0; the thread gives a nest_asyncio2 workaround, not tested here.
- Rename everywhere. Imports, but also logger names such as ib_insync.wrapper. Pickled objects fail too: a Stock pickled under ib_insync raised ModuleNotFoundError under ib_async.
- Search for qualifyContracts. Handle None at each call, and any code that assumed failures were left out of the list.
- Review status logic. Handle ValidationError and ApiUpdate as active, and Inactive as done. Check any handler keyed on error codes 202, 321, 329 or 105.
- Read state before disconnect. Anything that reads trades or positions after disconnect() now gets nothing.
- Decide on startup errors. With the default raiseSyncErrors=False, a timed-out sync request is only logged. Set it to True if a bot must not start on partial state.
- Recheck ports. The README's connection notes pair 7497 with TWS and 4001 with Gateway; by IBKR's defaults the first is paper and the second is live.
- Test on paper, by a person. Place, modify, reject and cancel an order, restart the gateway, and compare the library's state with TWS. Tests show how software behaves, not future results.
ib_async 3.0: plan for a second step
A protobuf-based 3.0 is in preparation and it changes more than 2.x did, so pin 2.1.0 now and treat 3.0 as a separate migration.
On 5 November 2026 PyPI's latest was still 2.1.0. The next-protobuf branch (opens in a new tab) labels 3.0.0 unreleased, and a maintainer wrote on issue 243 the day before that it is releasing soon, with no date. As the branch stands, it needs Python 3.11, depends on protobuf 7.36.2 or newer, and targets server version 226. Its migration guide (opens in a new tab) lists three main breaks:
- Financial values become Decimal or None instead of float, so arithmetic and comparisons need checking.
- commission becomes commissionAndFees, with the old name deprecated.
- Records such as AccountValue and Position become frozen dataclasses, so tuple indexing stops working.
The branch also addresses the refused-modification problem above. All of this can change before release, so reread those files when it ships, and repeat the probes.
ib_async documentation, ib_insync documentation and getting help
Use each project's own site for its docs, and expect IBKR to help only with what its logs show.
ib_async's documentation (opens in a new tab) and ib_insync's (opens in a new tab) were both online on 5 November 2026; only the former will change. Questions about ib_async belong in its issue tracker (opens in a new tab). If you want to stay closer to IBKR, the official client and its TWSSyncWrapper are covered in the Python guide, at the cost of rewriting against a different interface and the GPL terms.
Order-state design applies whichever library you use; see trading bot development and broker API integration. A bot stuck on ib_insync can be quoted for review through trading bot code review; naming these libraries does not mean Fincode has built on them.
Frequently asked questions
Is ib_insync dead?
It no longer receives updates: its repository was archived on 14 March 2024 and its last release is 0.9.86 from July 2023. It may still connect, but this guide could not test it against a current TWS.
Does ib_async need the ibapi package?
Version 2.1.0 does not. Its README says it implements the protocol itself, and its install pulled only aeventkit, nest_asyncio, tzdata and numpy in the test.
Is migrating from ib_insync to ib_async just a rename?
No. The rename makes it run, but qualifyContracts, error handling, order statuses and state after disconnect behave differently, as the probes here show.
Which ib_async version should a live bot use today?
This guide gives no recommendation beyond pinning an exact version. On 5 November 2026 the released version was 2.1.0, and 3.0 was unreleased with larger breaking changes.
Is ib_async affiliated with Interactive Brokers?
No. IBKR says its note on these packages is neither an endorsement nor a criticism, and that it provides no coding help for them.
Sources
- PyPI: ib_async 2.1.0 (opens in a new tab)
- PyPI: ib_insync 0.9.86 (opens in a new tab)
- GitHub: ib_async repository and README (opens in a new tab)
- GitHub: ib_insync repository (archived, family notice) (opens in a new tab)
- ib_async documentation: Changelog (2.0.0 breaking changes, 1.0.0 notes) (opens in a new tab)
- ib_async next-protobuf branch: v3 migration guide (unreleased) (opens in a new tab)
- ib_async issue 243: refused modification marks a working order Cancelled (user report, 4 November 2026) (opens in a new tab)
- ib_async license (BSD 2-Clause) (opens in a new tab)
- IBKR TWS API: Third Party API Platforms (ib_insync and ib_async note) (opens in a new tab)
- IBKR TWS API: Error Codes (200, 202, 321, 110) (opens in a new tab)
- IBKR: API Software download page (official client versions) (opens in a new tab)
- ib_insync documentation (opens in a new tab)
- SPK (Capital Markets Board): Notice on unauthorized leveraged transactions, 23 February 2023 (Turkish) (opens in a new tab)
This guide is general information about software and markets, not investment, legal or tax advice. Automated trading can lose money, past or backtested performance does not guarantee future results, and the rules that apply depend on your jurisdiction and your exchange's terms. Sources, checks and corrections: see the editorial policy.