Skip to content

Feature: heartbeat-based RTT/latency measurement on a live EClient session #158

@deepentropy

Description

@deepentropy

Summary

Expose a way for downstream apps to measure round-trip latency to IB's servers on an already-established EClient session, ideally as a heartbeat the library can drive (or that the caller can drive on demand) without disturbing auth or market-data flow.

Use case

I'm building a Tauri/Svelte trading frontend on top of ibx and wanted to surface current network latency to IB in the UI:

  • One value shown in the connecting modal (so the user sees link quality before placing the first order).
  • A continuously-updated value in the app's top bar while the session is live.

There's currently no obvious primitive for this in ibx:

  • req_current_time (src/api/client/stubs.rs:32) is documented as gateway-local — it returns local system time and does not do a server round-trip, so it can't be used to measure RTT.
  • req_contract_details, req_mkt_data etc. do round-trip, but they're heavyweight and have side effects (subscriptions, rate-limit budgets) that make them unsuitable as a periodic ping.

What I tried (and why it failed)

Naively, I added a pre-flight + 10 s heartbeat that did:

```rust
let addr = format!("{host}:4001").to_socket_addrs()?.next()?;
let t0 = Instant::now();
TcpStream::connect_timeout(&addr, Duration::from_secs(3))?;
// drop the stream immediately — measuring just the TCP handshake
```

…to the IB auth port (AUTH_PORT = 4001, from src/config.rs:19).

This breaks auth. Opening a TCP socket to :4001 and dropping it without sending the expected NS_SECURE_CONNECT (NS_VERSION) first message looks to IB's auth server like an aborted handshake. After a few of those probes the server starts rejecting subsequent real logins with:

```
Auth error: 1;user not configured on firewall;
```

— which is the catch-all rejection code raised at src/auth/session.rs:121, not literal allowlist misconfiguration. Same issue applies post-connect: probing :4001 periodically while a session is up is an anti-abuse footgun.

So bare TCP probing of IB's ports is not a viable approach for downstream apps.

Proposal

A first-class API for in-session RTT, something like one of:

  1. EClient::ping() -> io::Result<Duration> (or async): the library issues whatever IB-protocol message is cheapest and round-trips to a real IB server, returns the measured elapsed time. No side effects on subscriptions / contract caches / rate budgets.
  2. Built-in heartbeat with callback: EClient::start_heartbeat(interval) that periodically pings and surfaces the RTT through a new Wrapper::heartbeat(rtt: Duration) callback (or an event channel). The library would also use this internally as a liveness check and could expose last_rtt() / last_heartbeat_at() getters.
  3. Document a recommended pattern. If ibx already has a no-side-effects round-trip primitive I missed, a doc note in RUST_API.md covering "how to measure latency to IB on a live session" would be enough.

Option 2 is most ergonomic for downstream UIs because (a) it gives a free liveness signal alongside the latency number and (b) it centralizes the question of which IB message is safe to ping with — that decision is library-level, not app-level.

Notes

  • Whatever the underlying message, it must round-trip to IB (not gateway-local) and must not consume req_id/cancellation budgets that the caller needs for real requests.
  • It would be useful for both connect-time (single measurement once the session is up) and steady-state (heartbeat) use cases. A single primitive that supports both is fine.
  • For the connect-time case in particular, an early callback that reports the first successful round-trip RTT before the user starts placing orders is the most useful surface for trading-app UIs.

Happy to PR if you point me at the message you'd prefer the library use.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions