Skip to content

Deno FFI adapter has ~40x overhead vs native (~10x worse than Node) #126

Description

@momics

Summary

The Deno FFI adapter adds ~40x overhead compared to native HTTP, vs ~4x for the Node napi-rs adapter. A warm iroh request takes 2.5ms in Deno but only 284µs in Node — a 9x gap from the FFI bridge alone. The root cause is the poll-based architecture documented in ADR-009: Deno uses Deno.dlopen with synchronous FFI calls, requiring an mpsc polling loop to bridge async Rust operations. Node's napi-rs uses ThreadsafeFunction to push results directly to the JS event loop.

Evidence

Warm request (loopback, same machine, same iroh-http-core):

Runtime iroh native overhead
Node (napi-rs) 284µs 71µs 4.0x
Deno (FFI) 2.5ms 61µs 40.9x

Full benchmark comparison across all scenarios:

Scenario Node overhead Deno overhead Deno/Node ratio
cold-connect 1,223x 396x 0.3x (amortized by node init)
warm-request 4.0x 40.9x 10.2x
throughput-1kb 4.6x 38.3x 8.3x
throughput-64kb 10.9x 39.4x 3.6x
throughput-1mb 26.8x 56.6x 2.1x
multiplex-x8 2.8x 15.0x 5.3x
multiplex-x32 1.8x 6.7x 3.8x
serve-rps 4.4x 39.9x 9.0x

The Deno overhead is consistently 4–10x worse than Node for the same underlying Rust operations. This gap is entirely in the FFI bridge layer.

Rust core baseline (no FFI at all):

fetch_get_latency       128.7 µs

So: Rust=129µs → Node=284µs (2.2x FFI tax) → Deno=2,500ms (19.4x FFI tax). The Node bridge is nearly transparent; the Deno bridge adds ~2.2ms per operation.

Impact

  • Deno users experience 10x worse performance than Node users for the same API
  • Per-request overhead of ~2.4ms makes iroh-http impractical for Deno in latency-sensitive scenarios (e.g. API proxying, real-time data)
  • The gap undermines the cross-runtime promise — users may abandon Deno adapter entirely
  • Multiplex scenarios show the gap shrinks under concurrency (batching amortizes poll overhead), but single-request latency is the common case

Remediation

The fundamental issue is the polling architecture in the Deno FFI bridge. The current flow:

JS fetch() → sync FFI call → enqueue on Rust side → return poll token
JS poll loop (setInterval/setTimeout) → sync FFI poll() → check mpsc → return result or "pending"

Each operation requires multiple FFI round-trips. Potential approaches:

Option A: Deno.UnsafeCallback (push-based, like napi-rs)

Use Deno.UnsafeCallback to register a JS callback that Rust can invoke when an async operation completes. This mirrors napi-rs's ThreadsafeFunction.

Risk: Deno.UnsafeCallback has stability concerns and lifetime management complexity.

Option B: Shared memory + Atomics

Use SharedArrayBuffer with Atomics.wait/notify to block the JS side until Rust writes a result. Eliminates polling entirely.

Risk: Requires careful memory layout, and Atomics.wait blocks the thread.

Option C: Optimize the poll loop

Keep the current architecture but reduce overhead:

  • Batch multiple pending operations per poll call
  • Use adaptive poll intervals (immediate retry after first pending, then backoff)
  • Reduce FFI call overhead by passing results in pre-allocated shared buffers instead of JSON serialization

Risk: Incremental improvement, may not close the gap significantly.

Option D: WebSocket/IPC bridge

Replace FFI with a local WebSocket or Unix socket connection to a Rust subprocess. Async-native on both sides.

Risk: Adds process management complexity; may add latency from serialization.

Key files

  • packages/iroh-http-deno/ — Deno adapter (FFI bindings, poll loop)
  • packages/iroh-http-node/ — Node adapter (napi-rs, for comparison)
  • crates/iroh-http-adapter/ — shared Rust FFI bridge layer
  • docs/adr/009-ffi-bridge-reliability.md — documents the current architecture and known issues

Acceptance criteria

  1. Root cause profiled and quantified (where does the 2.2ms per-request overhead go? FFI call overhead? JSON serialization? Poll loop latency?)
  2. At least one alternative approach prototyped with benchmark comparison
  3. Deno warm-request overhead reduced to <10x native (from current 41x), or a documented architectural decision that Deno will have higher overhead with rationale
  4. Benchmark warm-request/iroh in Deno reflects the improvement

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1High prioritybugSomething isn't workingconnectivityPeer discovery and connection

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions