Skip to content

Runtime Compatibility

Connectum targets Node.js 20+ as the primary runtime. Bun compatibility is a secondary goal -- most packages work, but some features require workarounds or have known limitations. This page documents the current state of runtime compatibility across all @connectum/* packages.

Compatibility Matrix

PackageNode.js 20Node.js 22Node.js 25Bun
@connectum/coreYesYesYesYes
@connectum/interceptorsYesYesYesYes
@connectum/healthcheckYesYesYesYes
@connectum/reflectionYesYesYesYes
@connectum/authYesYesYesYes
@connectum/eventsYesYesYesYes
@connectum/events-natsYesYesYesYes
@connectum/events-kafkaYesYesYesYes
@connectum/events-redisYesYesYesYes
@connectum/events-amqpYesYesYesYes
@connectum/otelYesYesYesPartial
@connectum/cliYesYesYesYes
@connectum/testingYesYesYesPartial

Legend: Yes = fully supported, Partial = works with limitations (see details below).

HTTP/2 Client Transport

Node.js

On Node.js, HTTP/2 gRPC clients work out of the box with createGrpcTransport():

typescript
import { createClient } from '@connectrpc/connect';
import { createGrpcTransport } from '@connectrpc/connect-node';
import { GreeterService } from '#gen/greeter/v1/greeter_pb.js';

const transport = createGrpcTransport({
  baseUrl: 'http://localhost:5000',
  httpVersion: '2',
});

const client = createClient(GreeterService, transport);
const res = await client.sayHello({ name: 'Alice' });

This uses Node.js native node:http2 module for full HTTP/2 multiplexing.

Bun

Bun does not fully support the node:http2 client API. Calling createGrpcTransport() or createConnectTransport({ httpVersion: '2' }) from @connectrpc/connect-node throws a TypeError at runtime.

Workaround: use createConnectTransport() with httpVersion: '1.1' (the default):

typescript
import { createClient } from '@connectrpc/connect';
import { createConnectTransport } from '@connectrpc/connect-node';
import { GreeterService } from '#gen/greeter/v1/greeter_pb.js';

const transport = createConnectTransport({
  baseUrl: 'http://localhost:5000',
  // httpVersion defaults to '1.1' -- omit or set explicitly
});

const client = createClient(GreeterService, transport);
const res = await client.sayHello({ name: 'Alice' });

Limitations on Bun

  • No native gRPC protocol -- only the Connect protocol (JSON/Protobuf over HTTP/1.1) is available
  • No HTTP/2 multiplexing -- each request opens a separate connection
  • Server must support Connect protocol -- Connectum servers support it by default alongside gRPC

Streaming RPC

Node.js

On Node.js, both unary and streaming RPCs work with createGrpcTransport() or createConnectTransport():

typescript
import { createClient } from '@connectrpc/connect';
import { createGrpcTransport } from '@connectrpc/connect-node';
import { MonitorService } from '#gen/monitor/v1/monitor_pb.js';

const transport = createGrpcTransport({
  baseUrl: 'http://localhost:5000',
  httpVersion: '2',
});

const client = createClient(MonitorService, transport);

// Server streaming -- works with any transport
for await (const event of client.watchEvents({ filter: 'error' })) {
  console.log(`Event: ${event.type} -- ${event.message}`);
}

Bun

In Bun, streaming RPCs over createConnectTransport() from @connectrpc/connect-node may fail because the Node.js HTTP adapter does not work correctly in Bun's node:http compatibility layer. The solution is to build a transport that uses globalThis.fetch directly:

typescript
import { createClient } from '@connectrpc/connect';
import { createTransport } from '@connectrpc/connect/protocol-connect';
import { createFetchClient } from '@connectrpc/connect/protocol';
import { MonitorService } from '#gen/monitor/v1/monitor_pb.js';

// Use Bun's native fetch implementation
const transport = createTransport({
  baseUrl: 'http://localhost:5000',
  fetch: createFetchClient(globalThis.fetch),
  useBinaryFormat: true,
});

const client = createClient(MonitorService, transport);

// Server streaming -- uses Bun's native fetch with ReadableStream
for await (const event of client.watchEvents({ filter: 'error' })) {
  console.log(`Event: ${event.type} -- ${event.message}`);
}

Cross-Runtime Helper

You can create a helper function that selects the right transport based on the runtime:

typescript
import { createConnectTransport } from '@connectrpc/connect-node';

function createClientTransport(baseUrl: string) {
  // Bun sets globalThis.Bun
  if (typeof globalThis.Bun !== 'undefined') {
    // Dynamic import to avoid loading connect/protocol-connect on Node.js
    const { createTransport } = await import('@connectrpc/connect/protocol-connect');
    const { createFetchClient } = await import('@connectrpc/connect/protocol');
    return createTransport({
      baseUrl,
      fetch: createFetchClient(globalThis.fetch),
      useBinaryFormat: true,
    });
  }

  return createConnectTransport({
    baseUrl,
    httpVersion: '2',
  });
}

Testing Utilities

@connectum/testing is a public, production-ready package. Its mock helpers -- including createMockNext(), createMockNextError(), createMockNextSlow(), and the underlying createMockFn() spy -- are implemented on top of a portable spy factory that does not depend on node:test, so the same test code runs on Node.js, Bun, Deno, and bundler environments.

typescript
import { describe, it } from 'node:test'; // or 'bun:test'
import { createMockNext, createMockRequest } from '@connectum/testing';

describe('my interceptor', () => {
  it('calls next', async () => {
    const next = createMockNext();
    await myInterceptor(createMockRequest(), next);
    // next.mock.callCount() === 1
  });
});

createMockFn() is API-compatible with the subset of node:test's mock.fn() that the testing helpers rely on (.mock.calls, .mock.callCount()), so assertions written against one runtime work on the other. Full API: @connectum/testing.

OpenTelemetry

@connectum/otel depends on the official @opentelemetry/* SDK packages, which use node:perf_hooks, node:diagnostics_channel, and other Node.js-specific APIs.

FeatureNode.jsBun
Tracing (spans)YesPartial -- basic spans work, some auto-instrumentation may fail
MetricsYesPartial -- manual metrics work, automatic HTTP metrics may not
LoggingYesYes
Auto-instrumentationYesNo -- @opentelemetry/auto-instrumentations-node is not compatible

WARNING

If you use @connectum/otel on Bun, test your specific instrumentation setup thoroughly. Manual instrumentation (explicit span creation) is more reliable than auto-instrumentation on Bun.

Known Issues

IssueRuntimeStatusWorkaround
createGrpcTransport() throws TypeErrorBunOpen (Bun)Use createConnectTransport() with HTTP/1.1
createConnectTransport({ httpVersion: '2' }) throws TypeErrorBunOpen (Bun)Omit httpVersion or set to '1.1'
Streaming RPC fails with Node.js HTTP adapterBunOpen (Bun)Use createTransport + createFetchClient(globalThis.fetch)
node:test mock API unavailableBunBy designUse bun:test mock directly
OpenTelemetry auto-instrumentationBunOpen (OTel)Use manual instrumentation