kafka-crab-js 4.0.0-beta.2 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,907 +1,1131 @@
1
- <div align="center">
1
+ # Kafka Crab JS
2
2
 
3
- # 🦀 Kafka Crab JS 🦀
3
+ Kafka Crab JS is a native Kafka client for Node.js and TypeScript. It uses Rust, NAPI-RS, and librdkafka to reduce
4
+ JavaScript heap pressure, expose Kafka's mature native client behavior, and push high-throughput consumer workloads
5
+ through a small TypeScript-friendly API.
4
6
 
5
- A lightweight, flexible, and reliable Kafka client for JavaScript/TypeScript. It is built using Rust and librdkafka, providing a high-performance and feature-rich Kafka client.
6
-
7
- [![npm version](https://img.shields.io/npm/v/kafka-crab-js.svg)](https://www.npmjs.com/package/kafka-crab-js)
7
+ [![npm beta version](https://img.shields.io/badge/npm%20beta-v4.0.0--beta.3-blue.svg)](https://www.npmjs.com/package/kafka-crab-js)
8
8
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
9
9
 
10
- </div>
11
-
12
- ---
10
+ ## Version 4
11
+
12
+ `kafka-crab-js` v4 is the current beta line. It focuses on high-throughput consumers, lower JavaScript heap pressure,
13
+ and a cleaner monorepo release model under [`flash-tecnologia/crab-js`](https://github.com/flash-tecnologia/crab-js).
14
+
15
+ The main v4 additions are:
16
+
17
+ - Native Web Stream consumers through `client.createWebStreamConsumer()`, with explicit `serial` and `batch` modes.
18
+ - Direct batch consumption through `consumer.recvBatch(size, timeoutMs)` for workers that can process chunks.
19
+ - Compact native batch streaming internally, reducing native-to-JavaScript marshalling overhead before expanding to the
20
+ public `Message[]` API.
21
+ - Serial Web Stream prefetching, so message-by-message code still benefits from small native batches.
22
+ - Diagnostics-channel events in the core package, with OpenTelemetry tracing and metrics moved to the optional
23
+ `kafka-crab-js-otel` package.
24
+ - Updated TypeScript exports where runtime enum-like values are now string literal types, avoiding missing runtime
25
+ exports in ESM/CommonJS consumers.
26
+
27
+ For production adoption, treat v4 as a beta release: validate it with your broker settings, payload sizes, partition
28
+ count, commit strategy, and shutdown path before replacing an existing Kafka client.
29
+
30
+ ## Why Kafka Crab JS?
31
+
32
+ KafkaJS is a strong default for many Node.js services. Kafka Crab JS is for the cases where Kafka throughput, memory
33
+ pressure, native batching, or librdkafka behavior start to matter.
34
+
35
+ | Need | What Kafka Crab JS Provides |
36
+ | ----------------------------------- | ---------------------------------------------------------------------------------------- |
37
+ | Higher consumer throughput | Native batch receive paths and Web Stream batch consumers. |
38
+ | Lower JavaScript heap pressure | Message fetch and Kafka protocol work happen in Rust/librdkafka before crossing into JS. |
39
+ | Predictable Kafka offset handling | `commitMessage()` commits `message.offset + 1`, the value Kafka expects. |
40
+ | Production Kafka tuning | Advanced librdkafka settings are passed through with their original names. |
41
+ | Node.js ecosystem integration | Direct APIs, Node.js `Readable` streams, native Web Streams, and CommonJS/ESM exports. |
42
+ | Observability without core coupling | Diagnostics-channel events power the optional `kafka-crab-js-otel` package. |
43
+
44
+ In the local isolated consumer benchmark snapshot, `kafka-crab-js v4 (stream, batch)` reached about
45
+ `1.17M messages/sec`, ahead of the measured `@platformatic/kafka` and KafkaJS batch scenarios, while using lower RSS
46
+ delta and peak heap than both. Treat benchmark numbers as workload-specific; the full methodology and memory/GC
47
+ breakdown are documented in [BENCHMARKS.md](../../BENCHMARKS.md) and [Performance Benchmarks](#performance-benchmarks).
48
+
49
+ ## Highlights
50
+
51
+ - Native librdkafka client exposed through a TypeScript-friendly API.
52
+ - Producer, direct consumer, Node.js `Readable` stream, and v4 native Web Stream consumer APIs.
53
+ - High-throughput batch receive APIs for workloads that can process more than one message at a time.
54
+ - Compact batch streaming and serial prefetching to reduce native boundary crossings.
55
+ - Manual commit helpers that commit `message.offset + 1` correctly.
56
+ - Optional diagnostics-channel instrumentation for OpenTelemetry through `kafka-crab-js-otel`.
57
+ - Advanced librdkafka settings are forwarded through `configuration` without forcing a custom allowlist.
58
+
59
+ ## Requirements
60
+
61
+ - Node.js `>= 22`.
62
+ - A Kafka broker reachable from the Node.js process.
63
+ - No separate librdkafka install is required for the published binaries.
64
+
65
+ ## Table Of Contents
66
+
67
+ 1. [Version 4](#version-4)
68
+ 2. [Installation](#installation)
69
+ 3. [Module Usage](#module-usage)
70
+ 4. [Quick Start](#quick-start)
71
+ 5. [Message Model](#message-model)
72
+ 6. [Choosing A Consumer API](#choosing-a-consumer-api)
73
+ 7. [Producer API](#producer-api)
74
+ 8. [Consumer API](#consumer-api)
75
+ 9. [Stream Consumers](#stream-consumers)
76
+ 10. [Batching, Backpressure, And Tuning](#batching-backpressure-and-tuning)
77
+ 11. [Configuration](#configuration)
78
+ 12. [OpenTelemetry](#opentelemetry)
79
+ 13. [Performance Benchmarks](#performance-benchmarks)
80
+ 14. [Migration Notes](#migration-notes)
81
+ 15. [Troubleshooting](#troubleshooting)
82
+ 16. [Development](#development)
83
+ 17. [License](#license)
13
84
 
14
- ## What's New in Version 4.0.0
85
+ ## Installation
15
86
 
16
- ### BREAKING CHANGES ⚠️
87
+ ```bash
88
+ npm install kafka-crab-js
89
+ ```
17
90
 
18
- **`CommitMode`, `KafkaEventName`, `PartitionPosition`, and `SecurityProtocol` are no longer runtime exports from `kafka-crab-js`.**
91
+ ```bash
92
+ pnpm add kafka-crab-js
93
+ ```
19
94
 
20
- These names are now type-only at the root package API. This change applies to the `kafka-crab-js` root import surface and is intended as a major-version change.
95
+ ```bash
96
+ yarn add kafka-crab-js
97
+ ```
21
98
 
22
- #### Migration from v3.x
99
+ ## Module Usage
23
100
 
24
- **Before (v3.x):**
101
+ The package publishes both ESM and CommonJS entry points:
25
102
 
26
103
  ```ts
27
- import { CommitMode, SecurityProtocol } from 'kafka-crab-js'
104
+ import { KafkaClient } from 'kafka-crab-js'
105
+ import type { Message, ProducerRecord } from 'kafka-crab-js'
28
106
  ```
29
107
 
30
- **After for type-only usage:**
31
-
32
- ```ts
33
- import type { CommitMode, SecurityProtocol } from 'kafka-crab-js'
108
+ ```js
109
+ const { KafkaClient } = require('kafka-crab-js')
34
110
  ```
35
111
 
36
- **After for runtime usage:**
112
+ The public TypeScript types are generated from the native NAPI contract and the JavaScript wrapper. Runtime enum-like
113
+ values such as commit modes and security protocols are string literals, not runtime objects:
37
114
 
38
115
  ```ts
39
- const commitMode: 'Sync' | 'Async' = 'Sync'
40
- const securityProtocol: 'Plaintext' | 'Ssl' | 'SaslPlaintext' | 'SaslSsl' = 'Plaintext'
41
- const eventName: 'PreRebalance' | 'PostRebalance' | 'CommitCallback' = 'PreRebalance'
42
- const partitionPosition: 'Beginning' | 'End' | 'Stored' | 'Invalid' = 'Beginning'
116
+ await consumer.commitMessage(message, 'Sync')
117
+
118
+ const client = new KafkaClient({
119
+ brokers: 'localhost:9092',
120
+ securityProtocol: 'Plaintext',
121
+ })
43
122
  ```
44
123
 
45
- #### Replacement Values
124
+ ## Quick Start
46
125
 
47
- | Name | Replacement string literal values |
48
- | ------------------- | ------------------------------------------------------- |
49
- | `CommitMode` | `'Sync' \| 'Async'` |
50
- | `KafkaEventName` | `'PreRebalance' \| 'PostRebalance' \| 'CommitCallback'` |
51
- | `PartitionPosition` | `'Beginning' \| 'End' \| 'Stored' \| 'Invalid'` |
52
- | `SecurityProtocol` | `'Plaintext' \| 'Ssl' \| 'SaslPlaintext' \| 'SaslSsl'` |
126
+ ### Produce Messages
53
127
 
54
- ## What's New in Version 3.0.0
128
+ ```ts
129
+ import { KafkaClient } from 'kafka-crab-js'
55
130
 
56
- ### BREAKING CHANGES ⚠️
131
+ const client = new KafkaClient({
132
+ brokers: 'localhost:9092',
133
+ clientId: 'orders-api',
134
+ securityProtocol: 'Plaintext',
135
+ })
57
136
 
58
- **OpenTelemetry instrumentation has been moved to a separate package: `kafka-crab-js-otel`**
137
+ const producer = client.createProducer()
59
138
 
60
- This change reduces the core package size and makes OTEL an opt-in dependency.
139
+ const metadata = await producer.send({
140
+ topic: 'orders',
141
+ messages: [
142
+ {
143
+ key: Buffer.from('order-123'),
144
+ payload: Buffer.from(JSON.stringify({ id: 'order-123', status: 'created' })),
145
+ headers: {
146
+ 'content-type': Buffer.from('application/json'),
147
+ },
148
+ },
149
+ ],
150
+ })
61
151
 
62
- #### Migration from v2.x
152
+ console.log(metadata)
153
+ ```
63
154
 
64
- **Before (v2.x):**
155
+ ### Consume One Message At A Time
65
156
 
66
- ```javascript
157
+ ```ts
67
158
  import { KafkaClient } from 'kafka-crab-js'
68
159
 
69
160
  const client = new KafkaClient({
70
161
  brokers: 'localhost:9092',
71
- clientId: 'my-app',
72
- otel: {
73
- serviceName: 'my-service',
74
- metrics: { enabled: true },
162
+ clientId: 'orders-worker',
163
+ securityProtocol: 'Plaintext',
164
+ })
165
+
166
+ const consumer = client.createConsumer({
167
+ groupId: 'orders-worker',
168
+ enableAutoCommit: false,
169
+ configuration: {
170
+ 'auto.offset.reset': 'earliest',
75
171
  },
76
172
  })
77
- ```
78
173
 
79
- **After (v3.x):**
174
+ await consumer.subscribe('orders')
80
175
 
81
- ```javascript
82
- import { KafkaClient } from 'kafka-crab-js'
83
- import { enableOtelInstrumentation, endSpan } from 'kafka-crab-js-otel'
176
+ try {
177
+ while (true) {
178
+ const message = await consumer.recv()
179
+ if (!message) {
180
+ break
181
+ }
84
182
 
85
- // 1. Enable OTEL instrumentation BEFORE creating client
86
- // Note: serviceName is set via OTEL SDK Resource, not here
87
- enableOtelInstrumentation({
88
- metrics: { enabled: true },
89
- })
183
+ const value = JSON.parse(message.payload.toString('utf8'))
184
+ console.log({ value, topic: message.topic, partition: message.partition, offset: message.offset })
90
185
 
91
- // 2. Create client with diagnostics enabled
92
- const client = new KafkaClient({
93
- brokers: 'localhost:9092',
94
- clientId: 'my-app',
95
- diagnostics: true, // Required for OTEL to receive events
186
+ await consumer.commitMessage(message, 'Sync')
187
+ }
188
+ } finally {
189
+ await consumer.disconnect()
190
+ }
191
+ ```
192
+
193
+ ### Consume In Batches
194
+
195
+ ```ts
196
+ const consumer = client.createConsumer({
197
+ groupId: 'orders-batch-worker',
198
+ enableAutoCommit: false,
199
+ configuration: {
200
+ 'auto.offset.reset': 'earliest',
201
+ },
96
202
  })
97
203
 
98
- // 3. For consumers, call endSpan() when processing is complete
99
- const message = await consumer.recv()
100
- // ... process message ...
101
- endSpan(message)
204
+ await consumer.subscribe('orders')
205
+
206
+ try {
207
+ while (true) {
208
+ const batch = await consumer.recvBatch(500, 50)
209
+ if (batch.length === 0) {
210
+ continue
211
+ }
212
+
213
+ for (const message of batch) {
214
+ await processOrder(message.payload)
215
+ await consumer.commitMessage(message, 'Async')
216
+ }
217
+ }
218
+ } finally {
219
+ await consumer.disconnect()
220
+ }
102
221
  ```
103
222
 
104
- #### Key Changes
223
+ ## Message Model
105
224
 
106
- | v2.x | v3.x |
107
- | ---------------------------- | ------------------------------------------------------- |
108
- | `otel` config in KafkaClient | `enableOtelInstrumentation()` from `kafka-crab-js-otel` |
109
- | Automatic span ending | Call `endSpan(message)` manually for consumers |
110
- | N/A | `diagnostics: true` required in KafkaClient config |
111
- | OTEL bundled | Install `kafka-crab-js-otel` separately |
225
+ Messages use `Buffer` values because the Kafka protocol treats keys, payloads, and headers as bytes. Decode them at the
226
+ edge of your application instead of assuming text:
112
227
 
113
- #### New Package Structure
228
+ ```ts
229
+ import type { Message, MessageProducer, RecordMetadata } from 'kafka-crab-js'
230
+
231
+ const outgoing: MessageProducer = {
232
+ key: Buffer.from('order-123'),
233
+ payload: Buffer.from(JSON.stringify({ id: 'order-123' })),
234
+ headers: {
235
+ 'content-type': Buffer.from('application/json'),
236
+ source: Buffer.from('orders-api'),
237
+ },
238
+ }
114
239
 
115
- | Package | Description |
116
- | -------------------- | ------------------------------------------------ |
117
- | `kafka-crab-js` | Core Kafka client (producer, consumer, streams) |
118
- | `kafka-crab-js-otel` | OpenTelemetry instrumentation (separate install) |
240
+ function decodeJsonMessage(message: Message) {
241
+ return JSON.parse(message.payload.toString('utf8')) as unknown
242
+ }
119
243
 
120
- ### Internal Improvements
244
+ function logDelivery(records: RecordMetadata[]) {
245
+ for (const record of records) {
246
+ console.log(`${record.topic}[${record.partition}]@${record.offset}`)
247
+ }
248
+ }
249
+ ```
121
250
 
122
- - Uses Node.js `diagnostics_channel` for observability (zero overhead when not subscribed)
123
- - Cleaner separation of concerns between core and observability
124
- - Smaller bundle size for users who don't need OTEL
251
+ `Message` values contain the consumed Kafka offset. When committing manually, commit the next offset. Prefer
252
+ `commitMessage()` unless you intentionally need to calculate offsets yourself:
125
253
 
126
- ---
254
+ ```ts
255
+ await consumer.commitMessage(message, 'Sync')
256
+ await consumer.commit(message.topic, message.partition, message.offset + 1, 'Async')
257
+ ```
127
258
 
128
- ## Previous Versions
259
+ Keys are optional. An omitted key keeps Kafka producer partitioning semantics for keyless records. An empty
260
+ `Buffer.alloc(0)` is still a present key.
129
261
 
130
- <details>
131
- <summary>What's New in Version 2.1.0</summary>
262
+ ## Choosing A Consumer API
132
263
 
133
- ### New Features
264
+ | API | Emits | Best For |
265
+ | ---------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------ |
266
+ | `consumer.recv()` | `Message \| null` | Simple sequential workers and explicit control. |
267
+ | `consumer.recvBatch()` | `Message[]` | High-throughput workers that can process chunks. |
268
+ | `consumer.recvStream()` | Web `ReadableStream<Message>` | Pull-based Web Stream workflows. |
269
+ | `consumer.recvBatchStream()` | Web `ReadableStream<Message[]>` | Native batch streaming with explicit consumer control. |
270
+ | `client.createStreamConsumer()` | Node.js `Readable` emitting `Message` | Existing Node stream pipelines. |
271
+ | `client.createWebStreamConsumer()` | Web `ReadableStream<Message>` or `ReadableStream<Message[]>` | v4 high-throughput stream consumers. |
134
272
 
135
- 1. **Simplified Message Commits with `commitMessage()`**:
136
- - New convenience method that accepts a message and commit mode directly
137
- - Automatically handles `offset + 1` increment internally - no more manual offset arithmetic
138
- - Available on both `KafkaConsumer` and stream consumers
139
- - **Before** (v2.0.0):
140
- ```javascript
141
- const message = await consumer.recv()
142
- await consumer.commit(message.topic, message.partition, message.offset + 1, 'Sync')
143
- ```
144
- - **After** (v2.1.0):
145
- ```javascript
146
- const message = await consumer.recv()
147
- await consumer.commitMessage(message, 'Sync')
148
- ```
273
+ Two details matter for choosing correctly:
149
274
 
150
- 2. **Enhanced OpenTelemetry Support**:
151
- - Improved OTEL context propagation for better distributed tracing
152
- - Safe handling when OTEL SDK is not installed (no-op behavior)
153
- - Better span context management across producer and consumer operations
154
- - Seamless integration with standard OTEL SDK setup
275
+ - `createStreamConsumer()` is a Node.js `Readable` compatibility API. Even when `batchSize > 1`, it emits individual
276
+ `Message` objects and uses batching internally to reduce native boundary crossings.
277
+ - `createWebStreamConsumer()` returns a discriminated object. In serial mode it emits `Message`; in batch mode it emits
278
+ `Message[]`. Use this API when your code can process whole batches directly.
155
279
 
156
- 3. **CI/CD Improvements**:
157
- - Updated to Node.js 24 support
158
- - GitHub Actions updated to v6
159
- - Improved caching with actions/cache v4
280
+ Recommended starting points:
160
281
 
161
- </details>
282
+ - Use `consumer.recv()` for straightforward workers where one handler processes one message and then commits.
283
+ - Use `consumer.recvBatch()` when the handler can process chunks, flush work in groups, or commit after a batch.
284
+ - Use `createStreamConsumer()` when existing code already expects a Node.js `Readable`.
285
+ - Use `createWebStreamConsumer()` for new v4 stream code, especially when you can keep batch chunks intact.
162
286
 
163
- <details>
164
- <summary>What's New in Version 2.0.0</summary>
287
+ ## Producer API
165
288
 
166
- ### BREAKING CHANGES ⚠️
289
+ Create a producer from a `KafkaClient`:
167
290
 
168
- 1. **Consumer Configuration API Changes**:
169
- - **REMOVED**: `createTopic` field from `ConsumerConfiguration`
170
- - **Migration**: Use `createTopic` field in `TopicPartitionConfig` instead when subscribing to topics
291
+ ```ts
292
+ const producer = client.createProducer({
293
+ autoFlush: true,
294
+ queueTimeout: 5000,
295
+ configuration: {
296
+ 'compression.type': 'lz4',
297
+ acks: 'all',
298
+ },
299
+ })
300
+ ```
171
301
 
172
- 2. **Stream Lifecycle Management**:
173
- - Stream consumers now properly implement Node.js stream lifecycle methods (`_destroy()`)
174
- - **Memory leak prevention**: Streams now automatically disconnect Kafka consumers during destruction
302
+ Send one or more messages:
175
303
 
176
- 3. **Async Consumer Commit**:
177
- - **BREAKING**: The `consumer.commit()` method is now async and must be awaited
304
+ ```ts
305
+ const records = await producer.send({
306
+ topic: 'orders',
307
+ messages: [
308
+ { key: Buffer.from('1'), payload: Buffer.from('first') },
309
+ { key: Buffer.from('2'), payload: Buffer.from('second') },
310
+ ],
311
+ })
312
+ ```
178
313
 
179
- </details>
314
+ When `autoFlush` is enabled, `send()` waits for delivery confirmation and returns `RecordMetadata[]`. When `autoFlush`
315
+ is disabled, `send()` buffers messages and `flush()` sends pending messages:
180
316
 
181
- ---
317
+ ```ts
318
+ const producer = client.createProducer({ autoFlush: false })
182
319
 
183
- ## Features
320
+ await producer.send({
321
+ topic: 'orders',
322
+ messages: [{ payload: Buffer.from('buffered') }],
323
+ })
184
324
 
185
- - 🦀 Simple and intuitive API
186
- - 🚀 High-performance message processing
187
- - 🔄 Automatic reconnection handling
188
- - 🎯 Type-safe interfaces (TypeScript support)
189
- - ⚡ Async/await support
190
- - 🛠️ Configurable consumer and producer options
191
- - 📊 Stream processing support with configurable stream options
192
- - 📦 Message batching capabilities
193
- - 🔍 Comprehensive error handling
194
- - 📈 Performance benchmarking suite
195
- - 🔧 Flexible configuration system supporting complex data types
325
+ const records = await producer.flush()
326
+ console.log(records)
327
+ ```
196
328
 
197
- ## Table of Contents
329
+ `producer.inFlightCount()` returns the number of messages sent but not yet acknowledged.
198
330
 
199
- 1. [Installation](#installation)
200
- 2. [Quick Start](#quick-start)
201
- 3. [Consumer Examples](#basic-consumer-setup)
202
- 4. [Producer Examples](#producer-examples)
203
- 5. [Stream Processing](#stream-processing)
204
- 6. [Configuration](#configuration)
205
- 7. [Performance Benchmarks](#performance-benchmarks)
206
- 8. [Best Practices](#best-practices)
207
- 9. [Contributing](#contributing)
208
- 10. [License](#license)
209
- 11. [OpenTelemetry Instrumentation](#opentelemetry-instrumentation)
331
+ ### Delivery Semantics
210
332
 
211
- ## Installation
333
+ `send()` writes records to librdkafka and, with the default `autoFlush: true`, waits for delivery results before
334
+ resolving. Delivery metadata can still contain a per-record `error`, so production code should inspect it when failed
335
+ records must be retried or reported:
212
336
 
213
- ```bash
214
- npm install kafka-crab-js
215
- # or
216
- yarn add kafka-crab-js
217
- # or
218
- pnpm add kafka-crab-js
219
- ```
337
+ ```ts
338
+ const records = await producer.send({
339
+ topic: 'orders',
340
+ messages: [{ payload: Buffer.from('created') }],
341
+ })
220
342
 
221
- ## Quick Start
343
+ for (const record of records) {
344
+ if (record.error) {
345
+ throw new Error(`Kafka delivery failed: ${record.error.message}`)
346
+ }
347
+ }
348
+ ```
222
349
 
223
- ### Basic Consumer Setup
350
+ With `autoFlush: false`, `send()` returns an empty array and leaves delivery confirmation to the next `flush()` call.
351
+ This can improve throughput for bursty producers, but shutdown code must flush before the process exits:
224
352
 
225
- ```javascript
226
- import { KafkaClient } from 'kafka-crab-js'
227
- async function run() {
228
- const kafkaClient = new KafkaClient({
229
- brokers: 'localhost:29092',
230
- clientId: 'foo-client',
231
- logLevel: 'debug',
232
- brokerAddressFamily: 'v4',
233
- })
234
-
235
- // Create consumer with topic creation control
236
- const consumer = kafkaClient.createConsumer({
237
- groupId: 'foo-group',
238
- configuration: {
239
- 'auto.offset.reset': 'earliest',
240
- 'enable.auto.commit': false, // Use manual commit for better control
241
- },
242
- })
353
+ ```ts
354
+ import type { RecordMetadata } from 'kafka-crab-js'
243
355
 
244
- // Subscribe with topic creation options
245
- await consumer.subscribe([
246
- {
247
- topic: 'foo',
248
- createTopic: true,
249
- numPartitions: 3,
250
- },
251
- ])
252
-
253
- const message = await consumer.recv()
254
- const { payload, partition, offset, topic } = message
255
- console.log({
256
- topic,
257
- partition,
258
- offset,
259
- value: payload.toString(),
260
- })
261
-
262
- // Manual commit - two options:
263
- // Option 1 (v2.1.0+): Simplified with commitMessage
264
- await consumer.commitMessage(message, 'Sync')
356
+ const producer = client.createProducer({ autoFlush: false })
265
357
 
266
- // Option 2: Traditional commit with manual offset increment
267
- // await consumer.commit(topic, partition, offset + 1, 'Sync');
358
+ function assertAllDelivered(records: RecordMetadata[]) {
359
+ for (const record of records) {
360
+ if (record.error) {
361
+ throw new Error(`Kafka delivery failed: ${record.error.message}`)
362
+ }
363
+ }
364
+ }
268
365
 
269
- consumer.unsubscribe()
366
+ try {
367
+ await producer.send({ topic: 'orders', messages })
368
+ const records = await producer.flush()
369
+ assertAllDelivered(records)
370
+ } finally {
371
+ const remaining = await producer.flush()
372
+ assertAllDelivered(remaining)
270
373
  }
374
+ ```
375
+
376
+ ## Consumer API
377
+
378
+ ### Subscribe
379
+
380
+ The simplest subscription accepts a topic string:
271
381
 
272
- await run()
382
+ ```ts
383
+ await consumer.subscribe('orders')
273
384
  ```
274
385
 
275
- ### Basic Producer Setup
386
+ Use `TopicPartitionConfig[]` when you need offsets, partition-specific offsets, or topic creation:
276
387
 
277
- ```javascript
278
- import { KafkaClient } from 'kafka-crab-js'
388
+ ```ts
389
+ await consumer.subscribe([
390
+ {
391
+ topic: 'orders',
392
+ createTopic: true,
393
+ numPartitions: 3,
394
+ replicas: 1,
395
+ allOffsets: { position: 'Beginning' },
396
+ },
397
+ ])
398
+ ```
279
399
 
280
- const kafkaClient = new KafkaClient({
281
- brokers: 'localhost:29092',
282
- clientId: 'my-client-id',
283
- logLevel: 'info',
284
- brokerAddressFamily: 'v4',
285
- })
400
+ For partition-specific offsets:
286
401
 
287
- // Producer configuration is now optional with sensible defaults
288
- const producer = kafkaClient.createProducer({
289
- configuration: {
290
- 'message.timeout.ms': 5000, // Now supports number values
291
- 'batch.size': 16384,
292
- 'compression.type': 'snappy',
402
+ ```ts
403
+ await consumer.subscribe([
404
+ {
405
+ topic: 'orders',
406
+ partitionOffset: [
407
+ {
408
+ partition: 0,
409
+ offset: { offset: 42 },
410
+ },
411
+ ],
293
412
  },
294
- })
413
+ ])
414
+ ```
415
+
416
+ ### Receive
417
+
418
+ `recv()` waits for one message and returns `null` when the consumer is disconnected:
295
419
 
296
- const message = {
297
- id: 1,
298
- name: 'Sample Message',
299
- timestamp: new Date().toISOString(),
420
+ ```ts
421
+ const message = await consumer.recv()
422
+ if (message) {
423
+ await handleMessage(message)
300
424
  }
425
+ ```
301
426
 
302
- const result = await producer.send({
303
- topic: 'my-topic',
304
- messages: [
305
- {
306
- payload: Buffer.from(JSON.stringify(message)),
307
- },
308
- ],
427
+ `recvBatch(size, timeoutMs)` returns up to `size` messages:
428
+
429
+ ```ts
430
+ const messages = await consumer.recvBatch(1000, 100)
431
+ ```
432
+
433
+ An empty array means the timeout elapsed without a full batch. It does not mean the consumer is closed.
434
+
435
+ ### Commit
436
+
437
+ Use `commitMessage()` for normal message processing. It commits `message.offset + 1`, which is the offset Kafka expects:
438
+
439
+ ```ts
440
+ await consumer.commitMessage(message, 'Sync')
441
+ ```
442
+
443
+ Use `commit()` when you already calculated the offset:
444
+
445
+ ```ts
446
+ await consumer.commit(message.topic, message.partition, message.offset + 1, 'Async')
447
+ ```
448
+
449
+ Commit modes are string literal values:
450
+
451
+ ```ts
452
+ type CommitMode = 'Sync' | 'Async'
453
+ ```
454
+
455
+ For at-least-once processing, disable auto commit, process the message successfully, then commit. If processing fails
456
+ before the commit, Kafka can redeliver that message after restart or rebalance:
457
+
458
+ ```ts
459
+ const consumer = client.createConsumer({
460
+ groupId: 'orders-worker',
461
+ enableAutoCommit: false,
309
462
  })
310
463
 
311
- const errors = result.map((r) => r.error).filter(Boolean)
312
- if (errors.length > 0) {
313
- console.error('Error sending message:', errors)
314
- } else {
315
- console.log('Message sent. Offset:', result)
464
+ const message = await consumer.recv()
465
+ if (message) {
466
+ await processOrder(message)
467
+ await consumer.commitMessage(message, 'Sync')
316
468
  }
317
469
  ```
318
470
 
319
- ## Stream Processing
471
+ `'Sync'` waits for the broker commit response. `'Async'` schedules the commit through librdkafka and returns sooner,
472
+ which can be useful in high-throughput workers that tolerate the usual async commit tradeoff.
320
473
 
321
- ### Enhanced Stream Consumer Example
474
+ ### Pause, Resume, Seek, And Assignment
322
475
 
323
- ```javascript
324
- import { KafkaClient } from 'kafka-crab-js'
476
+ ```ts
477
+ consumer.pause()
478
+ consumer.resume()
325
479
 
326
- const kafkaClient = new KafkaClient({
327
- brokers: 'localhost:29092',
328
- clientId: 'my-client-id',
329
- logLevel: 'info',
330
- brokerAddressFamily: 'v4',
480
+ consumer.seek('orders', 0, { position: 'Beginning' })
481
+ consumer.seek('orders', 0, { offset: 42 })
482
+
483
+ const assignment = consumer.assignment()
484
+ const subscription = consumer.getSubscription()
485
+ ```
486
+
487
+ Consumer events are available through `onEvents()`:
488
+
489
+ ```ts
490
+ consumer.onEvents((error, event) => {
491
+ if (error) {
492
+ console.error(error)
493
+ return
494
+ }
495
+
496
+ console.log(event.name, event.payload)
331
497
  })
498
+ ```
332
499
 
333
- // Stream consumer with custom ReadableOptions (v2.0.0+)
334
- const kafkaStream = kafkaClient.createStreamConsumer(
335
- {
336
- groupId: `my-group-id`,
337
- enableAutoCommit: true,
338
- },
339
- {
340
- objectMode: true, // Default in v2.0.0+
341
- highWaterMark: 1024,
342
- encoding: null,
343
- },
344
- )
500
+ Event names are currently `PreRebalance`, `PostRebalance`, and `CommitCallback`. The payload includes the topic-partition
501
+ list associated with the event and may include an `error` string for commit or rebalance failures.
345
502
 
346
- await kafkaStream.subscribe([
347
- { topic: 'foo', createTopic: true },
348
- { topic: 'bar', createTopic: true },
349
- ])
503
+ ### Cleanup
504
+
505
+ Always disconnect direct consumers:
506
+
507
+ ```ts
508
+ try {
509
+ await consumer.subscribe('orders')
510
+ // consume...
511
+ } finally {
512
+ await consumer.disconnect()
513
+ }
514
+ ```
515
+
516
+ ## Stream Consumers
517
+
518
+ ### Node.js Readable Stream
350
519
 
351
- kafkaStream.on('data', (message) => {
352
- console.log('>>> Message received:', {
353
- payload: message.payload.toString(),
354
- offset: message.offset,
355
- partition: message.partition,
356
- topic: message.topic,
357
- })
520
+ Use `createStreamConsumer()` when integrating with Node.js stream tooling. It emits `Message` objects.
358
521
 
359
- if (message.offset > 10) {
360
- kafkaStream.destroy()
522
+ ```ts
523
+ const stream = client.createStreamConsumer({
524
+ groupId: 'orders-stream',
525
+ enableAutoCommit: false,
526
+ batchSize: 256,
527
+ batchTimeout: 50,
528
+ configuration: {
529
+ 'auto.offset.reset': 'earliest',
530
+ },
531
+ })
532
+
533
+ await stream.subscribe('orders')
534
+
535
+ stream.on('data', async (message) => {
536
+ try {
537
+ await processOrder(message.payload)
538
+ await stream.commitMessage(message, 'Async')
539
+ } catch (error) {
540
+ stream.destroy(error instanceof Error ? error : new Error(String(error)))
361
541
  }
362
542
  })
363
543
 
364
- kafkaStream.on('close', () => {
365
- kafkaStream.unsubscribe()
366
- console.log('Stream ended')
544
+ stream.on('error', (error) => {
545
+ console.error('stream error', error)
546
+ })
547
+
548
+ stream.on('close', () => {
549
+ console.log('stream closed')
367
550
  })
368
551
  ```
369
552
 
370
- ## Producer Examples
553
+ Destroy stream consumers instead of calling `disconnect()` directly. The stream cleanup path cancels the source reader,
554
+ unsubscribes, and disconnects the native consumer:
371
555
 
372
- ### Batch Message Production
556
+ ```ts
557
+ stream.destroy()
558
+ ```
373
559
 
374
- ```javascript
375
- const kafkaClient = new KafkaClient({
376
- brokers: 'localhost:29092',
377
- clientId: 'my-client-id',
378
- brokerAddressFamily: 'v4',
379
- })
560
+ ### Native Web Stream
380
561
 
381
- // Enhanced producer with flexible configuration
382
- const producer = kafkaClient.createProducer({
562
+ Use `createWebStreamConsumer()` when you want a Web Stream and clear serial/batch typing.
563
+
564
+ Serial mode is selected when `batchSize` is omitted, `0`, or `1`:
565
+
566
+ ```ts
567
+ const webConsumer = client.createWebStreamConsumer({
568
+ groupId: 'orders-web-serial',
569
+ serialPrefetchSize: 64,
570
+ serialPrefetchTimeout: 1,
383
571
  configuration: {
384
- 'batch.size': 50000, // Number value supported
385
- 'linger.ms': 10, // Number value supported
386
- 'compression.type': 'lz4',
387
- 'enable.idempotence': true, // Boolean value supported
572
+ 'auto.offset.reset': 'earliest',
388
573
  },
389
574
  })
390
575
 
391
- const messages = Array.from({ length: 100 }, (_, i) => ({
392
- payload: Buffer.from(
393
- JSON.stringify({
394
- _id: i,
395
- name: `Batch Message ${i}`,
396
- timestamp: new Date().toISOString(),
397
- }),
398
- ),
399
- }))
576
+ await webConsumer.consumer.subscribe('orders')
577
+
578
+ const reader = webConsumer.stream.getReader()
400
579
 
401
580
  try {
402
- const result = await producer.send({
403
- topic: 'my-topic',
404
- messages,
405
- })
406
- console.log('Batch sent. Offset:', result)
407
- console.assert(result.length === 100)
408
- } catch (error) {
409
- console.error('Batch error:', error)
581
+ const { value: message, done } = await reader.read()
582
+ if (!done && message) {
583
+ await processOrder(message.payload)
584
+ await webConsumer.consumer.commitMessage(message, 'Async')
585
+ }
586
+ } finally {
587
+ await reader.cancel()
588
+ await webConsumer.consumer.disconnect()
410
589
  }
411
590
  ```
412
591
 
413
- ### Producer with Keys and Headers
592
+ Batch mode is selected with `batchSize > 1` and emits `Message[]` chunks:
414
593
 
415
- ```javascript
416
- async function produceWithMetadata() {
417
- const producer = kafkaClient.createProducer({
418
- configuration: {
419
- acks: 'all',
420
- retries: 5,
421
- 'max.in.flight.requests.per.connection': 1,
422
- },
423
- })
594
+ ```ts
595
+ const webConsumer = client.createWebStreamConsumer({
596
+ groupId: 'orders-web-batch',
597
+ batchSize: 1024,
598
+ batchTimeout: 10,
599
+ configuration: {
600
+ 'auto.offset.reset': 'earliest',
601
+ },
602
+ })
603
+
604
+ if (webConsumer.mode === 'batch') {
605
+ await webConsumer.consumer.subscribe('orders')
606
+ const reader = webConsumer.stream.getReader()
424
607
 
425
608
  try {
426
- await producer.send({
427
- topic: 'user-events',
428
- messages: [
429
- {
430
- key: 'user-123',
431
- payload: Buffer.from(
432
- JSON.stringify({
433
- userId: 123,
434
- action: 'update',
435
- }),
436
- ),
437
- headers: {
438
- 'correlation-id': 'txn-123',
439
- source: 'user-service',
440
- },
441
- },
442
- ],
443
- })
444
- } catch (error) {
445
- console.error('Error:', error)
609
+ const { value: batch, done } = await reader.read()
610
+ if (!done && batch) {
611
+ for (const message of batch) {
612
+ await processOrder(message.payload)
613
+ }
614
+ }
615
+ } finally {
616
+ await reader.cancel()
617
+ await webConsumer.consumer.disconnect()
446
618
  }
447
619
  }
448
620
  ```
449
621
 
450
- ### Reconnecting Kafka Consumer
622
+ ## Batching, Backpressure, And Tuning
451
623
 
452
- ```javascript
453
- import { KafkaClient } from 'kafka-crab-js'
624
+ The main throughput lever is how many messages cross the native-to-JavaScript boundary per call.
454
625
 
455
- const kafkaClient = new KafkaClient({
456
- brokers: 'localhost:29092',
457
- clientId: 'reconnect-test',
458
- logLevel: 'debug',
459
- brokerAddressFamily: 'v4',
626
+ | Knob | Applies To | Effect |
627
+ | ----------------------- | ----------------------------------------------- | ---------------------------------------------------------------------- |
628
+ | `recvBatch(size, ms)` | Direct consumer | Pulls up to `size` messages, waiting up to `ms` for data. |
629
+ | `batchSize` | Node stream and Web Stream batch modes | Sets the maximum native batch size used by the stream. |
630
+ | `batchTimeout` | Node stream and Web Stream batch modes | Bounds how long a partially filled batch waits before being emitted. |
631
+ | `serialPrefetchSize` | `createWebStreamConsumer()` serial mode | Pulls small native batches and flattens them into individual messages. |
632
+ | `serialPrefetchTimeout` | `createWebStreamConsumer()` serial mode | Timeout for the serial-mode prefetch batch. |
633
+ | `streamOptions` | `createStreamConsumer()` Node.js `Readable` API | Lets Node stream `highWaterMark` participate in backpressure. |
634
+
635
+ Use larger batches when throughput matters and your handler can process arrays efficiently. Use smaller batches and
636
+ shorter timeouts when tail latency matters more than total throughput. Very large batches can increase RSS because more
637
+ payloads, keys, headers, and metadata must be retained at once.
638
+
639
+ The v4 Web Stream serial path defaults to `serialPrefetchSize: 64` and `serialPrefetchTimeout: 1`. This keeps the public
640
+ serial API message-by-message while reducing native boundary crossings. Batch mode defaults `batchTimeout` to `1000`
641
+ milliseconds when it is omitted.
642
+
643
+ For broker fetch tuning, pass librdkafka settings through `configuration`. Keep these values aligned with your expected
644
+ message size:
645
+
646
+ ```ts
647
+ const consumer = client.createConsumer({
648
+ groupId: 'orders-worker',
460
649
  configuration: {
461
- 'auto.offset.reset': 'earliest',
462
- 'session.timeout.ms': 30000,
463
- 'heartbeat.interval.ms': 10000,
650
+ 'fetch.min.bytes': 1,
651
+ 'fetch.wait.max.ms': 10,
652
+ 'fetch.max.bytes': 1_048_576,
653
+ 'max.partition.fetch.bytes': 1_048_576,
654
+ 'message.max.bytes': 1_000_000,
464
655
  },
465
656
  })
657
+ ```
466
658
 
467
- /**
468
- * Creates and configures a new Kafka stream consumer
469
- */
470
- async function createConsumer() {
471
- const kafkaStream = kafkaClient.createStreamConsumer(
472
- {
473
- groupId: 'reconnect-test',
474
- enableAutoCommit: true,
475
- },
476
- {
477
- highWaterMark: 100,
478
- objectMode: true,
479
- },
480
- )
481
-
482
- await kafkaStream.subscribe([
483
- { topic: 'foo', createTopic: true },
484
- { topic: 'bar', createTopic: true },
485
- ])
486
- return kafkaStream
487
- }
488
-
489
- /**
490
- * Starts a Kafka consumer with auto-restart capability
491
- */
492
- async function startConsumer() {
493
- let counter = 0
494
- let retryCount = 0
495
- const MAX_RETRIES = 5
496
- const RETRY_DELAY = 5000 // 5 seconds
497
-
498
- async function handleRetry() {
499
- if (retryCount < MAX_RETRIES) {
500
- retryCount++
501
- console.log(
502
- `Attempting to restart consumer (attempt ${retryCount}/${MAX_RETRIES}) in ${RETRY_DELAY / 1000} seconds...`,
503
- )
504
- setTimeout(setupConsumerWithRetry, RETRY_DELAY)
505
- } else {
506
- console.error(`Maximum retry attempts (${MAX_RETRIES}) reached. Stopping consumer.`)
507
- process.exit(1)
508
- }
509
- }
659
+ `fetch.max.bytes` must be greater than or equal to `message.max.bytes`. If this invariant is broken, librdkafka rejects
660
+ the consumer configuration before the benchmark or application starts.
510
661
 
511
- async function setupConsumerWithRetry() {
512
- try {
513
- const kafkaStream = await createConsumer()
514
- retryCount = 0 // Reset retry count on successful connection
515
-
516
- console.log('Starting consumer')
517
-
518
- kafkaStream.on('data', (message) => {
519
- counter++
520
- console.log('>>> Message received:', {
521
- counter,
522
- payload: message.payload.toString(),
523
- offset: message.offset,
524
- partition: message.partition,
525
- topic: message.topic,
526
- })
527
- })
528
-
529
- kafkaStream.on('error', async (error) => {
530
- console.error('Stream error:', error)
531
- handleRetry()
532
- })
533
-
534
- kafkaStream.on('close', () => {
535
- console.log('Stream ended')
536
- try {
537
- kafkaStream.unsubscribe()
538
- } catch (unsubError) {
539
- console.error('Error unsubscribing:', unsubError)
540
- }
541
- })
542
- } catch (error) {
543
- console.error('Error setting up consumer:', error)
544
- handleRetry()
545
- }
546
- }
662
+ ## Configuration
547
663
 
548
- await setupConsumerWithRetry()
549
- }
664
+ ### KafkaClient
550
665
 
551
- await startConsumer()
666
+ ```ts
667
+ const client = new KafkaClient({
668
+ brokers: 'localhost:9092',
669
+ clientId: 'orders-service',
670
+ securityProtocol: 'Plaintext',
671
+ logLevel: 'info',
672
+ brokerAddressFamily: 'v4',
673
+ diagnostics: true,
674
+ configuration: {
675
+ 'socket.keepalive.enable': true,
676
+ },
677
+ })
552
678
  ```
553
679
 
554
- ### Examples
680
+ | Option | Type | Default | Description |
681
+ | --------------------- | ------------------------------------------------------ | --------- | ------------------------------------------------------------------------- |
682
+ | `brokers` | `string` | required | Comma-separated broker list, for example `localhost:9092,localhost:9093`. |
683
+ | `clientId` | `string` | `rdkafka` | Client identifier sent to Kafka. |
684
+ | `securityProtocol` | `'Plaintext' \| 'Ssl' \| 'SaslPlaintext' \| 'SaslSsl'` | | Security protocol. |
685
+ | `logLevel` | `string` | `error` | librdkafka log level. |
686
+ | `brokerAddressFamily` | `string` | `v4` | Address family hint such as `v4` or `any`. |
687
+ | `diagnostics` | `boolean` | `true` | Enables diagnostic-channel events used by `kafka-crab-js-otel`. |
688
+ | `configuration` | `Record<string, any>` | | Additional librdkafka client settings. |
555
689
 
556
- You can find some examples on the [example](https://github.com/flash-tecnologia/kafka-crab-js/tree/main/example) folder of this project.
690
+ `configuration` is passed through to librdkafka after values are converted to strings. Use the original librdkafka
691
+ property names, for example `sasl.mechanism`, `queued.min.messages`, or `fetch.wait.max.ms`.
557
692
 
558
- ## Configuration
693
+ Common connection examples:
559
694
 
560
- ### KafkaConfiguration
695
+ ```ts
696
+ const localClient = new KafkaClient({
697
+ brokers: 'localhost:9092',
698
+ clientId: 'orders-local',
699
+ securityProtocol: 'Plaintext',
700
+ brokerAddressFamily: 'v4',
701
+ })
702
+ ```
561
703
 
562
- | Property | Type | Default | Description |
563
- | --------------------- | --------------------- | ----------- | ------------------------------------------------------------------- |
564
- | `brokers` | `string` | | List of brokers to connect to |
565
- | `clientId` | `string` | `"rdkafka"` | Client id to use for the connection |
566
- | `securityProtocol` | `SecurityProtocol` | | Security protocol to use (PLAINTEXT, SSL, SASL_PLAINTEXT, SASL_SSL) |
567
- | `logLevel` | `string` | `info` | Log level for the client |
568
- | `brokerAddressFamily` | `string` | `"v4"` | Address family to use for the connection (v4, v6) |
569
- | `configuration` | `Record<string, any>` | `{}` | Additional configuration options for the client |
570
- | `diagnostics` | `boolean` | `false` | **v3.0.0+**: Enable diagnostics channel for OTEL instrumentation |
704
+ ```ts
705
+ const saslClient = new KafkaClient({
706
+ brokers: process.env.KAFKA_BROKERS!,
707
+ clientId: 'orders-worker',
708
+ securityProtocol: 'SaslSsl',
709
+ configuration: {
710
+ 'sasl.mechanism': 'PLAIN',
711
+ 'sasl.username': process.env.KAFKA_USERNAME!,
712
+ 'sasl.password': process.env.KAFKA_PASSWORD!,
713
+ },
714
+ })
715
+ ```
571
716
 
572
717
  ### ConsumerConfiguration
573
718
 
574
- | Property | Type | Default | Description |
575
- | ---------------------- | --------------------- | ------- | ----------------------------------------- |
576
- | `groupId` | `string` | | Consumer group ID |
577
- | `enableAutoCommit` | `boolean` | `true` | Enable automatic offset commits |
578
- | `configuration` | `Record<string, any>` | `{}` | Additional consumer configuration options |
579
- | `fetchMetadataTimeout` | `number` | `60000` | Timeout for fetching metadata (ms) |
580
- | `maxBatchMessages` | `number` | `1000` | Maximum messages in a batch operation |
719
+ ```ts
720
+ const consumer = client.createConsumer({
721
+ groupId: 'orders-worker',
722
+ enableAutoCommit: false,
723
+ fetchMetadataTimeout: 5000,
724
+ configuration: {
725
+ 'auto.offset.reset': 'earliest',
726
+ 'enable.auto.commit': false,
727
+ 'fetch.min.bytes': 1,
728
+ },
729
+ })
730
+ ```
581
731
 
582
- ### Consumer Commit Methods
732
+ | Option | Type | Description |
733
+ | ---------------------- | --------------------- | ------------------------------------------ |
734
+ | `groupId` | `string` | Kafka consumer group id. |
735
+ | `enableAutoCommit` | `boolean` | Convenience flag for auto commit behavior. |
736
+ | `fetchMetadataTimeout` | `number` | Metadata fetch timeout in milliseconds. |
737
+ | `configuration` | `Record<string, any>` | Additional librdkafka consumer settings. |
583
738
 
584
- kafka-crab-js provides two methods for committing offsets:
739
+ `enableAutoCommit` is a convenience option that sets librdkafka `enable.auto.commit`. You can also pass
740
+ `'enable.auto.commit'` in `configuration`, but using both with different values makes the intent hard to read.
585
741
 
586
- | Method | Signature | Description |
587
- | --------------- | ---------------------------------------- | ----------------------------------------------------------------------- |
588
- | `commit` | `commit(topic, partition, offset, mode)` | Traditional commit - you must calculate `offset + 1` |
589
- | `commitMessage` | `commitMessage(message, mode)` | **v2.1.0+**: Simplified commit - automatically handles offset increment |
742
+ For new services that need explicit processing guarantees, start with manual commits:
590
743
 
591
- ```javascript
592
- // Using commitMessage (recommended for v2.1.0+)
593
- const message = await consumer.recv()
594
- await consumer.commitMessage(message, 'Sync')
744
+ ```ts
745
+ const consumer = client.createConsumer({
746
+ groupId: 'orders-worker',
747
+ enableAutoCommit: false,
748
+ configuration: {
749
+ 'auto.offset.reset': 'earliest',
750
+ 'enable.partition.eof': false,
751
+ },
752
+ })
753
+ ```
595
754
 
596
- // Using commit (traditional)
597
- const message = await consumer.recv()
598
- await consumer.commit(message.topic, message.partition, message.offset + 1, 'Sync')
755
+ ### ProducerConfiguration
756
+
757
+ ```ts
758
+ const producer = client.createProducer({
759
+ autoFlush: true,
760
+ queueTimeout: 5000,
761
+ configuration: {
762
+ 'compression.type': 'lz4',
763
+ },
764
+ })
599
765
  ```
600
766
 
601
- Both methods support `'Sync'` and `'Async'` commit modes.
767
+ | Option | Type | Description |
768
+ | --------------- | --------------------- | ------------------------------------------------------- |
769
+ | `autoFlush` | `boolean` | When enabled, `send()` waits for delivery confirmation. |
770
+ | `queueTimeout` | `number` | Queue timeout in milliseconds. |
771
+ | `configuration` | `Record<string, any>` | Additional librdkafka producer settings. |
602
772
 
603
- ### ProducerConfiguration
773
+ Producer settings are workload-specific. A low-latency producer might prefer smaller buffering windows, while a
774
+ throughput-oriented producer can allow librdkafka to coalesce more work:
604
775
 
605
- | Property | Type | Default | Description |
606
- | --------------- | --------------------- | ------- | ----------------------------------------- |
607
- | `queueTimeout` | `number` | `5000` | Queue timeout in milliseconds |
608
- | `autoFlush` | `boolean` | `true` | Enable automatic message flushing |
609
- | `configuration` | `Record<string, any>` | `{}` | Additional producer configuration options |
776
+ ```ts
777
+ const producer = client.createProducer({
778
+ autoFlush: false,
779
+ queueTimeout: 5000,
780
+ configuration: {
781
+ acks: 'all',
782
+ 'compression.type': 'lz4',
783
+ 'queue.buffering.max.ms': 10,
784
+ 'queue.buffering.max.messages': 100000,
785
+ },
786
+ })
787
+ ```
610
788
 
611
789
  ### TopicPartitionConfig
612
790
 
613
- | Property | Type | Default | Description |
614
- | ----------------- | ------------------------ | ------- | ----------------------------------------------------- |
615
- | `topic` | `string` | | Topic name |
616
- | `allOffsets` | `OffsetModel` | | Offset configuration for all partitions |
617
- | `partitionOffset` | `Array<PartitionOffset>` | | Per-partition offset configuration |
618
- | `createTopic` | `boolean` | `false` | **v2.0.0+**: Create topic if it doesn't exist |
619
- | `numPartitions` | `number` | `1` | **v2.0.0+**: Number of partitions when creating topic |
620
- | `replicas` | `number` | `1` | **v2.0.0+**: Number of replicas when creating topic |
791
+ | Option | Type | Description |
792
+ | ----------------- | ------------------- | ------------------------------------------------ |
793
+ | `topic` | `string` | Topic name. |
794
+ | `allOffsets` | `OffsetModel` | Offset model applied to all assigned partitions. |
795
+ | `partitionOffset` | `PartitionOffset[]` | Partition-specific offset model. |
796
+ | `createTopic` | `boolean` | Create the topic before subscribing. |
797
+ | `numPartitions` | `number` | Partition count when creating a topic. |
798
+ | `replicas` | `number` | Replica count when creating a topic. |
621
799
 
622
- You can see the available options here: [librdkafka](https://docs.confluent.io/platform/current/clients/librdkafka/html/md_CONFIGURATION.html).
800
+ Offset models:
623
801
 
624
- ### OpenTelemetry Instrumentation
802
+ ```ts
803
+ {
804
+ position: 'Beginning'
805
+ }
806
+ {
807
+ position: 'End'
808
+ }
809
+ {
810
+ position: 'Stored'
811
+ }
812
+ {
813
+ position: 'Invalid'
814
+ }
815
+ {
816
+ offset: 42
817
+ }
818
+ ```
625
819
 
626
- > **Note:** Starting with v3.0.0, OpenTelemetry instrumentation has been moved to a separate package: [`kafka-crab-js-otel`](https://www.npmjs.com/package/kafka-crab-js-otel)
820
+ ### Type-Only Literals
627
821
 
628
- #### Installation
822
+ In v4, these names are TypeScript-only exports:
823
+
824
+ ```ts
825
+ import type { CommitMode, KafkaEventName, PartitionPosition, SecurityProtocol } from 'kafka-crab-js'
826
+ ```
827
+
828
+ Use literal values at runtime:
829
+
830
+ ```ts
831
+ const commitMode: CommitMode = 'Sync'
832
+ const securityProtocol: SecurityProtocol = 'Plaintext'
833
+ ```
834
+
835
+ ## OpenTelemetry
836
+
837
+ OpenTelemetry support lives in the separate `kafka-crab-js-otel` package. The core package emits diagnostic-channel
838
+ events when `diagnostics !== false`; the OTEL package subscribes to those events.
839
+
840
+ Install the instrumentation package and OpenTelemetry dependencies:
629
841
 
630
842
  ```bash
631
- npm install kafka-crab-js-otel @opentelemetry/api
843
+ npm install kafka-crab-js-otel @opentelemetry/api @opentelemetry/sdk-node
632
844
  ```
633
845
 
634
- #### Usage
846
+ Enable instrumentation before creating `KafkaClient`:
635
847
 
636
- ```javascript
848
+ ```ts
637
849
  import { KafkaClient } from 'kafka-crab-js'
638
850
  import { enableOtelInstrumentation, endSpan } from 'kafka-crab-js-otel'
639
851
 
640
- // Enable instrumentation before creating client
641
- // Note: serviceName is configured via OTEL SDK Resource
642
852
  enableOtelInstrumentation({
643
853
  metrics: { enabled: true },
644
854
  })
645
855
 
646
- // Create client with diagnostics enabled
647
856
  const client = new KafkaClient({
648
857
  brokers: 'localhost:9092',
649
- clientId: 'my-app',
858
+ clientId: 'orders-worker',
650
859
  diagnostics: true,
651
860
  })
652
861
 
653
- // For consumers, call endSpan() when done processing
862
+ const consumer = client.createConsumer({ groupId: 'orders-worker' })
863
+ await consumer.subscribe('orders')
864
+
654
865
  const message = await consumer.recv()
655
- // ... process message ...
656
- endSpan(message)
866
+ if (message) {
867
+ try {
868
+ await processOrder(message.payload)
869
+ } finally {
870
+ endSpan(message)
871
+ }
872
+ }
657
873
  ```
658
874
 
659
- For complete configuration options and examples, see the [kafka-crab-js-otel README](../kafka-crab-js-otel/README.md).
660
-
661
- #### Examples
662
-
663
- See comprehensive examples in the `example/` directory:
664
-
665
- - `example/otel-tracing-example.mjs` - Complete tracing setup with Jaeger
666
- - `example/otel-metrics-example.mjs` - Metrics collection with Prometheus
667
- - `example/README.md` - Full documentation for all examples
875
+ See the [OpenTelemetry package README](../kafka-crab-js-otel/README.md) for full tracing and metrics setup.
668
876
 
669
877
  ## Performance Benchmarks
670
878
 
671
- ### Running Benchmarks
879
+ The repository includes a benchmark suite that compares `kafka-crab-js` with KafkaJS and `@platformatic/kafka`. The
880
+ latest captured run in [BENCHMARKS.md](../../BENCHMARKS.md) was recorded on May 25, 2026 on a MacBook Pro with Apple M1,
881
+ Node.js `v24.16.0`, and a local three-broker `apache/kafka:4.0.0` cluster.
672
882
 
673
- kafka-crab-js v2.0.0+ includes a comprehensive benchmark suite to compare performance against other popular Kafka clients:
883
+ From the repository root:
674
884
 
675
885
  ```bash
676
- cd ../benchmark
886
+ podman compose up -d
677
887
 
678
- # Set up benchmark environment (requires Kafka running locally)
679
- vp install
680
- vp run setup:consumer
888
+ cd benchmarks/kafka
889
+
890
+ # Set up benchmark data. Requires Kafka running locally.
891
+ KAFKA_BROKERS=127.0.0.1:9092,127.0.0.1:9093,127.0.0.1:9094 vp run setup:consumer
681
892
 
682
- # Run consumer performance benchmarks
683
- vp run benchmark
893
+ # Run the default isolated-process memory benchmark.
894
+ KAFKA_BROKERS=127.0.0.1:9092,127.0.0.1:9093,127.0.0.1:9094 vp run benchmark
684
895
  ```
685
896
 
686
- ### Benchmark Results
897
+ The default benchmark runs one child Node.js process per selected scenario. This avoids carrying V8 heap pages, native
898
+ allocator arenas, sockets, librdkafka state, and Kafka metadata from one client into the next client's measurement.
687
899
 
688
- _Benchmarks run on macOS with Apple M1 chip processing 50,000 messages (December 2024)_
900
+ ### Consumer Benchmark Snapshot
689
901
 
690
- ```
691
- ╔════════════════════════╤═════════╤══════════════════╤═══════════╤══════════════════════════╗
692
- ║ Slower tests │ Samples │ Result │ Tolerance │ Difference with previous ║
693
- ╟────────────────────────┼─────────┼──────────────────┼───────────┼──────────────────────────╢
694
- ║ kafkajs │ 50000 │ 834.12 op/sec │ ± 0.22 % │ ║
695
- ║ node-rdkafka (evented) │ 84115 │ 24922.67 op/sec │ ± 74.82 % │ + 2887.91 % ║
696
- ║ kafka-crab-js (serial) │ 50000 │ 43213.86 op/sec │ ± 3.46 % │ + 73.39 % ║
697
- ║ node-rdkafka (stream) │ 50000 │ 49805.32 op/sec │ ± 27.10 % │ + 15.25 % ║
698
- ╟────────────────────────┼─────────┼──────────────────┼───────────┼──────────────────────────╢
699
- ║ Fastest test │ Samples │ Result │ Tolerance │ Difference with previous ║
700
- ╟────────────────────────┼─────────┼──────────────────┼───────────┼──────────────────────────╢
701
- ║ kafka-crab-js (batch) │ 50000 │ 205985.31 op/sec │ ± 16.53 % │ + 313.58 % ║
702
- ╚════════════════════════╧═════════╧══════════════════╧═══════════╧══════════════════════════╝
902
+ The captured run below is equivalent to:
903
+
904
+ ```bash
905
+ cd benchmarks/kafka
906
+ KAFKA_BROKERS=127.0.0.1:9092,127.0.0.1:9093,127.0.0.1:9094 vp run benchmark
703
907
  ```
704
908
 
705
- The benchmark suite compares:
909
+ The run uses the default isolated-process lifecycle-memory benchmark mode.
706
910
 
707
- - **kafka-crab-js (serial)**: Single message processing - **43,214 ops/sec**
708
- - **kafka-crab-js (batch)**: Batch message processing - **205,985 ops/sec** (fastest)
709
- - **node-rdkafka (evented)**: Event-based processing - **24,923 ops/sec**
710
- - **node-rdkafka (stream)**: Stream-based processing - **49,805 ops/sec**
711
- - **kafkajs**: Official KafkaJS client - **834 ops/sec**
911
+ ![Consumer benchmark snapshot](./assets/consumer-benchmark-snapshot.svg)
712
912
 
713
- Performance characteristics:
913
+ #### Throughput
714
914
 
715
- - **52x faster than kafkajs** in serial mode, **247x faster in batch mode**
716
- - **High throughput**: Batch processing provides 4.8x performance improvement over serial mode
717
- - **Low latency**: Optimized for both single and batch message processing
718
- - **Memory efficient**: Lock-free data structures minimize memory overhead
719
- - **Concurrent processing**: Zero-contention concurrent operations
915
+ | Rank | Scenario | Result | Relative |
916
+ | ---: | ----------------------------------- | --------------------: | -------: |
917
+ | 1 | `kafka-crab-js v4 (stream, batch)` | `1,167,396.71 op/sec` | `100.0%` |
918
+ | 2 | `@platformatic/kafka` | `653,355.35 op/sec` | `56.0%` |
919
+ | 3 | `kafka-crab-js v4 (stream, serial)` | `558,968.71 op/sec` | `47.9%` |
920
+ | 4 | `KafkaJS (eachBatch)` | `478,411.67 op/sec` | `41.0%` |
921
+ | 5 | `KafkaJS (eachMessage)` | `431,519.93 op/sec` | `37.0%` |
922
+ | 6 | `KafkaJS (eachMessage, concurrent)` | `427,265.29 op/sec` | `36.6%` |
720
923
 
721
- ### Benchmark Configuration
924
+ `kafka-crab-js v4 (stream, batch)` is the fastest scenario in this run. It is about `78.7%` faster than
925
+ `@platformatic/kafka` and about `144.0%` faster than `KafkaJS (eachBatch)`. Its tolerance was `+/- 4.40%`, so the exact
926
+ gap should still be treated as a benchmark snapshot rather than a universal constant.
722
927
 
723
- You can customize benchmark parameters in `benchmark/utils/definitions.ts`:
928
+ For message-by-message consumption, `kafka-crab-js v4 (stream, serial)` is close to KafkaJS throughput while using much
929
+ less lifecycle memory. In this run it is about `29.5%` faster than `KafkaJS (eachMessage)` and about `30.8%` faster
930
+ than the concurrent KafkaJS `eachMessage` scenario.
724
931
 
725
- ```typescript
726
- export const topic = 'benchmarks'
727
- export const brokers = ['localhost:9092', 'localhost:9093', 'localhost:9094']
932
+ #### Memory And GC
728
933
 
729
- // Benchmark parameters can be adjusted in consumer.ts:
730
- const iterations = 10_000 // Number of messages to process
731
- const maxBytes = 200 // Maximum message size
732
- ```
934
+ | Scenario | RSS delta | Peak heap | GC time | GC share | Notes |
935
+ | ----------------------------------- | ----------: | ---------: | ---------: | -------: | ------------------------------------------ |
936
+ | `kafka-crab-js v4 (stream, serial)` | `64.7 MiB` | `14.6 MiB` | `30.46 ms` | `3.41%` | Lowest lifecycle RSS and heap. |
937
+ | `kafka-crab-js v4 (stream, batch)` | `106.0 MiB` | `29.5 MiB` | `21.69 ms` | `5.06%` | Best throughput, efficiency, and GC time. |
938
+ | `KafkaJS (eachMessage, concurrent)` | `178.3 MiB` | `72.2 MiB` | `83.77 ms` | `7.16%` | Concurrency did not improve this workload. |
939
+ | `KafkaJS (eachMessage)` | `186.3 MiB` | `91.6 MiB` | `90.15 ms` | `7.78%` | Highest peak heap and max GC pause. |
940
+ | `KafkaJS (eachBatch)` | `193.4 MiB` | `83.8 MiB` | `63.05 ms` | `6.03%` | KafkaJS batch baseline. |
941
+ | `@platformatic/kafka` | `216.0 MiB` | `87.4 MiB` | `74.27 ms` | `9.71%` | Fastest non-crab competitor. |
733
942
 
734
- ## Best Practices
943
+ Lifecycle memory includes module loading, client creation, subscription, consumption, cleanup, and retained RSS after
944
+ cleanup. GC metrics use the narrower first-to-last-message window used for throughput.
735
945
 
736
- ### Error Handling
946
+ The most important memory result is efficiency, not just raw RSS. `kafka-crab-js v4 (stream, batch)` delivered
947
+ `11016 op/sec/MiB`, the best score in the run. `kafka-crab-js v4 (stream, serial)` delivered `8635 op/sec/MiB`, ahead
948
+ of `@platformatic/kafka` at `3025 op/sec/MiB` and `KafkaJS (eachBatch)` at `2474 op/sec/MiB`.
737
949
 
738
- - Always wrap async operations in try-catch blocks
739
- - Implement proper error logging and monitoring
740
- - Handle both operational and programming errors separately
950
+ ### Benchmark Interpretation
741
951
 
742
- ### Performance
952
+ - Use `kafka-crab-js v4 (stream, batch)` when raw throughput or memory efficiency is the priority.
953
+ - Use `kafka-crab-js v4 (stream, serial)` when message-by-message processing needs the lowest lifecycle RSS and heap
954
+ pressure.
955
+ - Compare batch scenarios with batch scenarios and message scenarios with message scenarios.
956
+ - `@platformatic/kafka` is the strongest non-crab throughput competitor in this run, but it used more RSS, heap, and GC
957
+ time than both v4 scenarios.
958
+ - KafkaJS `eachMessage` concurrency did not help this workload.
743
959
 
744
- - Use batch operations for high-throughput scenarios
745
- - Configure appropriate batch sizes and compression settings
746
- - Monitor and tune consumer group performance
747
- - Leverage the benchmark suite to optimize your specific use case
960
+ ### Benchmark Configuration
748
961
 
749
- ### Configuration (v2.0.0+)
962
+ Captured benchmark settings:
750
963
 
751
- - Use the flexible configuration system with proper data types:
752
- ```javascript
753
- const config = {
754
- 'batch.size': 16384, // number
755
- 'compression.type': 'snappy', // string
756
- 'enable.idempotence': true, // boolean
757
- retries: 5, // number
758
- }
759
- ```
964
+ ```bash
965
+ BENCHMARK_ITERATIONS=100000
966
+ BENCHMARK_RUNS=5
967
+ BENCHMARK_FETCH_MIN_BYTES=1
968
+ BENCHMARK_FETCH_WAIT_MS=10
969
+ BENCHMARK_MAX_BYTES=2048
970
+ BENCHMARK_BATCH_SIZE=4096
971
+ BENCHMARK_BATCH_TIMEOUT_MS=2
972
+ BENCHMARK_KAFKAJS_EACH_MESSAGE_CONCURRENCY=3
973
+ BENCHMARK_MEMORY=1
974
+ BENCHMARK_MEMORY_SAMPLE_MS=100
975
+ BENCHMARK_MEMORY_SETTLE_MS=100
976
+ BENCHMARK_TOPIC=benchmarks
977
+ BENCHMARK_PARTITIONS=3
978
+ ```
760
979
 
761
- ### Message Processing
980
+ See [BENCHMARKS.md](../../BENCHMARKS.md) for the latest captured run and the
981
+ [benchmark README](../../benchmarks/kafka/README.md) for the full benchmark methodology and environment variables.
762
982
 
763
- - Validate message formats before processing
764
- - Implement proper serialization/deserialization
765
- - Handle message ordering when required
766
- - Use topic creation options for better topic management
983
+ ## Migration Notes
767
984
 
768
- ### Stream Processing (v2.0.0+)
985
+ ### v4 Type-Only Runtime Exports
769
986
 
770
- - Configure appropriate `ReadableOptions` for your use case
771
- - Use `objectMode: true` for structured message processing
772
- - Set appropriate `highWaterMark` based on memory constraints
987
+ `CommitMode`, `KafkaEventName`, `PartitionPosition`, and `SecurityProtocol` are no longer runtime exports from
988
+ `kafka-crab-js`.
773
989
 
774
- ## Contributing
990
+ Before:
775
991
 
776
- 1. Fork the repository
777
- 2. Create your feature branch (`git checkout -b feature/amazing-feature`)
778
- 3. Run the benchmark suite to ensure performance isn't degraded
779
- 4. Commit your changes (`git commit -m 'Add some amazing feature'`)
780
- 5. Push to the branch (`git push origin feature/amazing-feature`)
781
- 6. Open a Pull Request
992
+ ```ts
993
+ import { CommitMode, SecurityProtocol } from 'kafka-crab-js'
994
+ ```
782
995
 
783
- ### Development Commands
996
+ After:
784
997
 
785
- ```bash
786
- # Build the project
787
- vp run build
998
+ ```ts
999
+ import type { CommitMode, SecurityProtocol } from 'kafka-crab-js'
788
1000
 
789
- # Run type checking
790
- vp run typecheck
1001
+ const commitMode: CommitMode = 'Sync'
1002
+ const securityProtocol: SecurityProtocol = 'Plaintext'
1003
+ ```
791
1004
 
792
- # Run linting and formatting
793
- vp check
1005
+ ### v3 OpenTelemetry Split
794
1006
 
795
- # Run benchmarks
796
- cd ../benchmark
797
- vp run benchmark
798
- ```
1007
+ OpenTelemetry instrumentation moved from the core package to `kafka-crab-js-otel` in v3. This keeps the core package
1008
+ small and makes tracing/metrics opt-in.
799
1009
 
800
- ## OpenTelemetry Instrumentation
1010
+ ## Troubleshooting
801
1011
 
802
- > **Note:** Starting with v3.0.0, OpenTelemetry instrumentation has been moved to a separate package: [`kafka-crab-js-otel`](https://www.npmjs.com/package/kafka-crab-js-otel)
1012
+ ### The Consumer Does Not Receive Existing Messages
803
1013
 
804
- Kafka Crab JS offers turnkey tracing for Kafka workloads:
1014
+ Use an offset reset policy and a fresh consumer group:
805
1015
 
806
- - **Seamless propagation** – Producer instrumentation injects `traceparent`/`tracestate` into Kafka headers while retaining any existing headers (including `Buffer` values) so downstream systems continue to see custom metadata.
807
- - **Consumer & stream coverage** – Standard consumers, batch consumers, and `createStreamConsumer` streams emit spans that include consumer group, topic, partition, offset, and batch size semantics.
808
- - **Hook-friendly spans** – Both `messageHook` and `producerHook` callbacks run inside the active span context, simplifying attribute decoration or error handling.
809
- - **Zero overhead when disabled** – Uses Node.js `diagnostics_channel` for near-zero cost when OTEL is not active.
1016
+ ```ts
1017
+ const consumer = client.createConsumer({
1018
+ groupId: `orders-worker-${Date.now()}`,
1019
+ configuration: {
1020
+ 'auto.offset.reset': 'earliest',
1021
+ },
1022
+ })
1023
+ ```
810
1024
 
811
- ### Consumer span lifecycle (important)
1025
+ Or subscribe with an explicit starting position:
812
1026
 
813
- Kafka Crab JS creates consumer `process <topic>` spans, but only your application knows when processing is complete.
1027
+ ```ts
1028
+ await consumer.subscribe([{ topic: 'orders', allOffsets: { position: 'Beginning' } }])
1029
+ ```
814
1030
 
815
- - For single-message consumers, call `endSpan(message)` when you're done processing the message.
816
- - For batch consumers, call `endSpan(batch)` when you're done processing the batch.
817
- - The `endSpan()` helper is exported from `kafka-crab-js-otel`.
1031
+ ### The Process Hangs On Shutdown
818
1032
 
819
- This closes the span(s) and (when metrics are enabled) records `messaging.process.duration`.
1033
+ Direct consumers should call `disconnect()`:
820
1034
 
821
- ### Global instrumentation (singleton)
1035
+ ```ts
1036
+ await consumer.disconnect()
1037
+ ```
822
1038
 
823
- `kafka-crab-js-otel` uses a process-wide OpenTelemetry instrumentation singleton.
1039
+ Node stream consumers should call `destroy()`:
824
1040
 
825
- - Call `enableOtelInstrumentation()` **before** creating any `KafkaClient` instances.
826
- - Creating multiple `KafkaClient` instances shares the same instrumentation.
827
- - For tests, use `resetKafkaInstrumentation()` to clear the singleton between runs.
1041
+ ```ts
1042
+ stream.destroy()
1043
+ ```
828
1044
 
829
- ### Minimal Setup Example
1045
+ Web stream consumers should cancel the reader and disconnect the underlying consumer:
830
1046
 
831
- When using stream consumers, the easiest pattern is calling `endSpan()` in a `finally` block:
1047
+ ```ts
1048
+ await reader.cancel()
1049
+ await webConsumer.consumer.disconnect()
1050
+ ```
1051
+
1052
+ ### Localhost Resolves To IPv6 But Kafka Listens On IPv4
1053
+
1054
+ Set `brokerAddressFamily: 'v4'`:
832
1055
 
833
1056
  ```ts
834
- import { KafkaClient } from 'kafka-crab-js'
835
- import { enableOtelInstrumentation, endSpan } from 'kafka-crab-js-otel'
836
- import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node'
837
- import { SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base'
838
- import { AsyncHooksContextManager } from '@opentelemetry/context-async-hooks'
839
- import { context } from '@opentelemetry/api'
840
-
841
- // 1. Set up OpenTelemetry SDK
842
- const provider = new NodeTracerProvider()
843
- provider.addSpanProcessor(new SimpleSpanProcessor(exporter))
844
- provider.register()
845
- context.setGlobalContextManager(new AsyncHooksContextManager().enable())
846
-
847
- // 2. Enable kafka-crab-js instrumentation BEFORE creating client
848
- enableOtelInstrumentation({
849
- captureMessagePayload: true,
850
- captureMessageHeaders: true,
851
- producerHook: (span) => span.setAttribute('messaging.client.kind', 'producer'),
1057
+ const client = new KafkaClient({
1058
+ brokers: 'localhost:9092',
1059
+ brokerAddressFamily: 'v4',
852
1060
  })
1061
+ ```
853
1062
 
854
- // 3. Create client with diagnostics enabled
1063
+ ### Need Advanced Kafka Settings
1064
+
1065
+ Pass librdkafka settings through `configuration`:
1066
+
1067
+ ```ts
855
1068
  const client = new KafkaClient({
856
- brokers: 'localhost:29092',
857
- clientId: 'orders-api',
858
- diagnostics: true, // Required for OTEL to receive events
1069
+ brokers: 'localhost:9092',
1070
+ configuration: {
1071
+ 'socket.keepalive.enable': true,
1072
+ 'metadata.max.age.ms': 300000,
1073
+ },
859
1074
  })
1075
+ ```
860
1076
 
861
- // 4. Use producer - spans are created automatically
862
- const producer = client.createProducer()
863
- await producer.send({
864
- topic: 'orders',
865
- messages: [
866
- {
867
- payload: Buffer.from(JSON.stringify({ orderId: '123' })),
868
- headers: { 'custom-header': 'foo' },
869
- },
870
- ],
871
- })
1077
+ Consumer and producer configuration objects also accept their own `configuration` maps.
872
1078
 
873
- // 5. Use stream consumer with proper cleanup
874
- const consumer = client.createStreamConsumer({
875
- groupId: 'orders-consumer',
876
- enableAutoCommit: false,
877
- })
1079
+ ### `fetch.max.bytes` Must Be At Least `message.max.bytes`
878
1080
 
879
- await consumer.subscribe('orders')
1081
+ librdkafka validates fetch limits at consumer creation time. If you tune fetch sizes for benchmarks or production and
1082
+ set `message.max.bytes` higher than `fetch.max.bytes`, the consumer will fail to start with an error similar to:
880
1083
 
881
- consumer.on('data', (message) => {
882
- try {
883
- console.log(message.headers?.['custom-header']?.toString())
884
- } finally {
885
- // IMPORTANT: close the "process <topic>" span when your app finishes handling the message
886
- endSpan(message)
887
- }
888
- })
1084
+ ```text
1085
+ `fetch.max.bytes` must be >= `message.max.bytes`
1086
+ ```
1087
+
1088
+ Keep the fetch caps aligned:
889
1089
 
890
- // Proper cleanup for stream consumers - use destroy() not disconnect()
891
- consumer.on('close', () => console.log('Consumer closed'))
892
- // consumer.destroy()
1090
+ ```ts
1091
+ const consumer = client.createConsumer({
1092
+ groupId: 'orders-worker',
1093
+ configuration: {
1094
+ 'message.max.bytes': 1_000_000,
1095
+ 'fetch.max.bytes': 1_048_576,
1096
+ 'max.partition.fetch.bytes': 1_048_576,
1097
+ },
1098
+ })
893
1099
  ```
894
1100
 
895
- To disable OTEL, simply don't call `enableOtelInstrumentation()` or set `diagnostics: false` in the client config.
1101
+ ### Benchmark Numbers Move Between Runs
896
1102
 
897
- ## License
1103
+ Kafka benchmarks are sensitive to CPU frequency, power mode, broker state, topic data already in page cache, and V8 heap
1104
+ history. The benchmark package defaults to isolated child processes for memory mode to reduce cross-scenario
1105
+ contamination, but you should still compare multiple runs and focus on large, repeatable gaps.
898
1106
 
899
- This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
1107
+ ## Development
900
1108
 
901
- ---
1109
+ From the repository root:
902
1110
 
903
- <div align="center">
1111
+ ```bash
1112
+ vp install
1113
+ vp run build
1114
+ vp run test
1115
+ vp run test:integration
1116
+ vp check
1117
+ ```
904
1118
 
905
- [![Built with Rust](https://img.shields.io/badge/Built%20with-Rust-orange)](https://www.rust-lang.org/)
1119
+ Useful package-local commands:
1120
+
1121
+ ```bash
1122
+ vp run --filter kafka-crab-js build
1123
+ vp run --filter kafka-crab-js test
1124
+ vp run --filter kafka-crab-js test:integration
1125
+ ```
1126
+
1127
+ Integration tests require Kafka at `localhost:9092` unless `KAFKA_BROKERS` is set.
1128
+
1129
+ ## License
906
1130
 
907
- </div>
1131
+ MIT