kafka-crab-js 3.1.0 → 4.0.0-beta.3

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.
Files changed (58) hide show
  1. package/README.md +605 -634
  2. package/assets/consumer-benchmark-snapshot.svg +189 -0
  3. package/dist/base-kafka-stream-readable-BokUK06Z.d.ts +69 -0
  4. package/dist/base-kafka-stream-readable-I1xda63L.d.cts +69 -0
  5. package/dist/channels-CYQ4ni8K.d.cts +133 -0
  6. package/dist/channels-QBobKRC2.d.ts +133 -0
  7. package/dist/diagnostics/channels.cjs +85 -0
  8. package/dist/diagnostics/channels.cjs.map +1 -0
  9. package/dist/diagnostics/channels.d.cts +2 -0
  10. package/dist/diagnostics/channels.d.ts +2 -138
  11. package/dist/diagnostics/channels.js +72 -0
  12. package/dist/diagnostics/channels.js.map +1 -0
  13. package/dist/diagnostics/index.cjs +21 -0
  14. package/dist/diagnostics/index.d.cts +3 -0
  15. package/dist/diagnostics/index.d.ts +3 -7
  16. package/dist/diagnostics/index.js +3 -0
  17. package/dist/diagnostics/instrumentation.cjs +334 -0
  18. package/dist/diagnostics/instrumentation.cjs.map +1 -0
  19. package/dist/diagnostics/instrumentation.d.cts +2 -0
  20. package/dist/diagnostics/instrumentation.d.ts +2 -31
  21. package/dist/diagnostics/instrumentation.js +329 -0
  22. package/dist/diagnostics/instrumentation.js.map +1 -0
  23. package/dist/index.cjs +47 -701
  24. package/dist/index.d.cts +8 -0
  25. package/dist/index.d.ts +8 -8
  26. package/dist/index.js +9 -662
  27. package/dist/instrumentation-BiIPAe1B.d.cts +37 -0
  28. package/dist/instrumentation-CwoJPHwZ.d.ts +37 -0
  29. package/dist/kafka-client.cjs +249 -0
  30. package/dist/kafka-client.cjs.map +1 -0
  31. package/dist/kafka-client.d.cts +107 -0
  32. package/dist/kafka-client.d.ts +98 -52
  33. package/dist/kafka-client.js +248 -0
  34. package/dist/kafka-client.js.map +1 -0
  35. package/dist/streams/base-kafka-stream-readable.cjs +109 -0
  36. package/dist/streams/base-kafka-stream-readable.cjs.map +1 -0
  37. package/dist/streams/base-kafka-stream-readable.d.cts +2 -0
  38. package/dist/streams/base-kafka-stream-readable.d.ts +2 -60
  39. package/dist/streams/base-kafka-stream-readable.js +108 -0
  40. package/dist/streams/base-kafka-stream-readable.js.map +1 -0
  41. package/dist/streams/kafka-batch-stream-readable.cjs +106 -0
  42. package/dist/streams/kafka-batch-stream-readable.cjs.map +1 -0
  43. package/dist/streams/kafka-batch-stream-readable.d.cts +46 -0
  44. package/dist/streams/kafka-batch-stream-readable.d.ts +40 -25
  45. package/dist/streams/kafka-batch-stream-readable.js +105 -0
  46. package/dist/streams/kafka-batch-stream-readable.js.map +1 -0
  47. package/dist/streams/kafka-stream-readable.cjs +63 -0
  48. package/dist/streams/kafka-stream-readable.cjs.map +1 -0
  49. package/dist/streams/kafka-stream-readable.d.cts +27 -0
  50. package/dist/streams/kafka-stream-readable.d.ts +22 -7
  51. package/dist/streams/kafka-stream-readable.js +62 -0
  52. package/dist/streams/kafka-stream-readable.js.map +1 -0
  53. package/js-binding.cjs +52 -52
  54. package/js-binding.d.ts +28 -0
  55. package/js-binding.js +52 -52
  56. package/package.json +54 -52
  57. package/dist/index.cjs.map +0 -7
  58. package/dist/index.js.map +0 -7
package/README.md CHANGED
@@ -1,838 +1,809 @@
1
- <div align="center">
1
+ # Kafka Crab JS
2
2
 
3
- # 🦀 Kafka Crab JS 🦀
4
-
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.
3
+ A high-performance Kafka client for Node.js and TypeScript, implemented with Rust, NAPI-RS, librdkafka, and a small
4
+ JavaScript API layer.
6
5
 
7
6
  [![npm version](https://img.shields.io/npm/v/kafka-crab-js.svg)](https://www.npmjs.com/package/kafka-crab-js)
8
7
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
9
- </div>
10
8
 
11
- ---
9
+ ## Highlights
10
+
11
+ - Native librdkafka client exposed through a TypeScript-friendly API.
12
+ - Producer, direct consumer, Node.js `Readable` stream, and native Web Stream consumer APIs.
13
+ - High-throughput batch receive APIs for workloads that can process more than one message at a time.
14
+ - Manual commit helpers that commit `message.offset + 1` correctly.
15
+ - Optional diagnostics-channel instrumentation for OpenTelemetry through `kafka-crab-js-otel`.
16
+ - Advanced librdkafka settings are forwarded through `configuration` without forcing a custom allowlist.
17
+
18
+ ## Requirements
12
19
 
13
- ## What's New in Version 3.0.0
20
+ - Node.js `>= 22`.
21
+ - A Kafka broker reachable from the Node.js process.
22
+ - No separate librdkafka install is required for the published binaries.
14
23
 
15
- ### BREAKING CHANGES ⚠️
24
+ ## Table Of Contents
25
+
26
+ 1. [Installation](#installation)
27
+ 2. [Quick Start](#quick-start)
28
+ 3. [Choosing A Consumer API](#choosing-a-consumer-api)
29
+ 4. [Producer API](#producer-api)
30
+ 5. [Consumer API](#consumer-api)
31
+ 6. [Stream Consumers](#stream-consumers)
32
+ 7. [Configuration](#configuration)
33
+ 8. [OpenTelemetry](#opentelemetry)
34
+ 9. [Performance Benchmarks](#performance-benchmarks)
35
+ 10. [Migration Notes](#migration-notes)
36
+ 11. [Troubleshooting](#troubleshooting)
37
+ 12. [Development](#development)
38
+ 13. [License](#license)
16
39
 
17
- **OpenTelemetry instrumentation has been moved to a separate package: `kafka-crab-js-otel`**
40
+ ## Installation
18
41
 
19
- This change reduces the core package size and makes OTEL an opt-in dependency.
42
+ ```bash
43
+ npm install kafka-crab-js
44
+ ```
20
45
 
21
- #### Migration from v2.x
46
+ ```bash
47
+ pnpm add kafka-crab-js
48
+ ```
49
+
50
+ ```bash
51
+ yarn add kafka-crab-js
52
+ ```
53
+
54
+ ## Quick Start
22
55
 
23
- **Before (v2.x):**
24
- ```javascript
56
+ ### Produce Messages
57
+
58
+ ```ts
25
59
  import { KafkaClient } from 'kafka-crab-js'
26
60
 
27
61
  const client = new KafkaClient({
28
62
  brokers: 'localhost:9092',
29
- clientId: 'my-app',
30
- otel: {
31
- serviceName: 'my-service',
32
- metrics: { enabled: true },
33
- },
63
+ clientId: 'orders-api',
64
+ securityProtocol: 'Plaintext',
34
65
  })
35
- ```
36
66
 
37
- **After (v3.x):**
38
- ```javascript
39
- import { KafkaClient } from 'kafka-crab-js'
40
- import { enableOtelInstrumentation, endSpan } from 'kafka-crab-js-otel'
67
+ const producer = client.createProducer()
41
68
 
42
- // 1. Enable OTEL instrumentation BEFORE creating client
43
- // Note: serviceName is set via OTEL SDK Resource, not here
44
- enableOtelInstrumentation({
45
- metrics: { enabled: true },
69
+ const metadata = await producer.send({
70
+ topic: 'orders',
71
+ messages: [
72
+ {
73
+ key: Buffer.from('order-123'),
74
+ payload: Buffer.from(JSON.stringify({ id: 'order-123', status: 'created' })),
75
+ headers: {
76
+ 'content-type': Buffer.from('application/json'),
77
+ },
78
+ },
79
+ ],
46
80
  })
47
81
 
48
- // 2. Create client with diagnostics enabled
82
+ console.log(metadata)
83
+ ```
84
+
85
+ ### Consume One Message At A Time
86
+
87
+ ```ts
88
+ import { KafkaClient } from 'kafka-crab-js'
89
+
49
90
  const client = new KafkaClient({
50
91
  brokers: 'localhost:9092',
51
- clientId: 'my-app',
52
- diagnostics: true, // Required for OTEL to receive events
92
+ clientId: 'orders-worker',
93
+ securityProtocol: 'Plaintext',
53
94
  })
54
95
 
55
- // 3. For consumers, call endSpan() when processing is complete
56
- const message = await consumer.recv()
57
- // ... process message ...
58
- endSpan(message)
96
+ const consumer = client.createConsumer({
97
+ groupId: 'orders-worker',
98
+ enableAutoCommit: false,
99
+ configuration: {
100
+ 'auto.offset.reset': 'earliest',
101
+ },
102
+ })
103
+
104
+ await consumer.subscribe('orders')
105
+
106
+ try {
107
+ while (true) {
108
+ const message = await consumer.recv()
109
+ if (!message) {
110
+ break
111
+ }
112
+
113
+ const value = JSON.parse(message.payload.toString('utf8'))
114
+ console.log({ value, topic: message.topic, partition: message.partition, offset: message.offset })
115
+
116
+ await consumer.commitMessage(message, 'Sync')
117
+ }
118
+ } finally {
119
+ await consumer.disconnect()
120
+ }
59
121
  ```
60
122
 
61
- #### Key Changes
123
+ ### Consume In Batches
62
124
 
63
- | v2.x | v3.x |
64
- |------|------|
65
- | `otel` config in KafkaClient | `enableOtelInstrumentation()` from `kafka-crab-js-otel` |
66
- | Automatic span ending | Call `endSpan(message)` manually for consumers |
67
- | N/A | `diagnostics: true` required in KafkaClient config |
68
- | OTEL bundled | Install `kafka-crab-js-otel` separately |
125
+ ```ts
126
+ const consumer = client.createConsumer({
127
+ groupId: 'orders-batch-worker',
128
+ enableAutoCommit: false,
129
+ configuration: {
130
+ 'auto.offset.reset': 'earliest',
131
+ },
132
+ })
133
+
134
+ await consumer.subscribe('orders')
69
135
 
70
- #### New Package Structure
136
+ try {
137
+ while (true) {
138
+ const batch = await consumer.recvBatch(500, 50)
139
+ if (batch.length === 0) {
140
+ continue
141
+ }
71
142
 
72
- | Package | Description |
73
- |---------|-------------|
74
- | `kafka-crab-js` | Core Kafka client (producer, consumer, streams) |
75
- | `kafka-crab-js-otel` | OpenTelemetry instrumentation (separate install) |
143
+ for (const message of batch) {
144
+ await processOrder(message.payload)
145
+ await consumer.commitMessage(message, 'Async')
146
+ }
147
+ }
148
+ } finally {
149
+ await consumer.disconnect()
150
+ }
151
+ ```
76
152
 
77
- ### Internal Improvements
78
- - Uses Node.js `diagnostics_channel` for observability (zero overhead when not subscribed)
79
- - Cleaner separation of concerns between core and observability
80
- - Smaller bundle size for users who don't need OTEL
153
+ ## Choosing A Consumer API
81
154
 
82
- ---
155
+ | API | Emits | Best For |
156
+ | ---------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------ |
157
+ | `consumer.recv()` | `Message \| null` | Simple sequential workers and explicit control. |
158
+ | `consumer.recvBatch()` | `Message[]` | High-throughput workers that can process chunks. |
159
+ | `consumer.recvStream()` | Web `ReadableStream<Message>` | Pull-based Web Stream workflows. |
160
+ | `consumer.recvBatchStream()` | Web `ReadableStream<Message[]>` | Native batch streaming with explicit consumer control. |
161
+ | `client.createStreamConsumer()` | Node.js `Readable` emitting `Message` | Existing Node stream pipelines. |
162
+ | `client.createWebStreamConsumer()` | Web `ReadableStream<Message>` or `ReadableStream<Message[]>` | v4 high-throughput stream consumers. |
83
163
 
84
- ## Previous Versions
164
+ Two details matter for choosing correctly:
85
165
 
86
- <details>
87
- <summary>What's New in Version 2.1.0</summary>
166
+ - `createStreamConsumer()` is a Node.js `Readable` compatibility API. Even when `batchSize > 1`, it emits individual
167
+ `Message` objects and uses batching internally to reduce native boundary crossings.
168
+ - `createWebStreamConsumer()` returns a discriminated object. In serial mode it emits `Message`; in batch mode it emits
169
+ `Message[]`. Use this API when your code can process whole batches directly.
88
170
 
89
- ### New Features
171
+ ## Producer API
90
172
 
91
- 1. **Simplified Message Commits with `commitMessage()`**:
92
- - New convenience method that accepts a message and commit mode directly
93
- - Automatically handles `offset + 1` increment internally - no more manual offset arithmetic
94
- - Available on both `KafkaConsumer` and stream consumers
95
- - **Before** (v2.0.0):
96
- ```javascript
97
- const message = await consumer.recv();
98
- await consumer.commit(message.topic, message.partition, message.offset + 1, 'Sync');
99
- ```
100
- - **After** (v2.1.0):
101
- ```javascript
102
- const message = await consumer.recv();
103
- await consumer.commitMessage(message, 'Sync');
104
- ```
173
+ Create a producer from a `KafkaClient`:
105
174
 
106
- 2. **Enhanced OpenTelemetry Support**:
107
- - Improved OTEL context propagation for better distributed tracing
108
- - Safe handling when OTEL SDK is not installed (no-op behavior)
109
- - Better span context management across producer and consumer operations
110
- - Seamless integration with standard OTEL SDK setup
175
+ ```ts
176
+ const producer = client.createProducer({
177
+ autoFlush: true,
178
+ queueTimeout: 5000,
179
+ configuration: {
180
+ 'compression.type': 'lz4',
181
+ acks: 'all',
182
+ },
183
+ })
184
+ ```
111
185
 
112
- 3. **CI/CD Improvements**:
113
- - Updated to Node.js 24 support
114
- - GitHub Actions updated to v6
115
- - Improved caching with actions/cache v4
186
+ Send one or more messages:
116
187
 
117
- </details>
188
+ ```ts
189
+ const records = await producer.send({
190
+ topic: 'orders',
191
+ messages: [
192
+ { key: Buffer.from('1'), payload: Buffer.from('first') },
193
+ { key: Buffer.from('2'), payload: Buffer.from('second') },
194
+ ],
195
+ })
196
+ ```
118
197
 
119
- <details>
120
- <summary>What's New in Version 2.0.0</summary>
198
+ When `autoFlush` is enabled, `send()` waits for delivery confirmation and returns `RecordMetadata[]`. When `autoFlush`
199
+ is disabled, `send()` buffers messages and `flush()` sends pending messages:
121
200
 
122
- ### BREAKING CHANGES ⚠️
201
+ ```ts
202
+ const producer = client.createProducer({ autoFlush: false })
123
203
 
124
- 1. **Consumer Configuration API Changes**:
125
- - **REMOVED**: `createTopic` field from `ConsumerConfiguration`
126
- - **Migration**: Use `createTopic` field in `TopicPartitionConfig` instead when subscribing to topics
204
+ await producer.send({
205
+ topic: 'orders',
206
+ messages: [{ payload: Buffer.from('buffered') }],
207
+ })
127
208
 
128
- 2. **Stream Lifecycle Management**:
129
- - Stream consumers now properly implement Node.js stream lifecycle methods (`_destroy()`)
130
- - **Memory leak prevention**: Streams now automatically disconnect Kafka consumers during destruction
209
+ const records = await producer.flush()
210
+ console.log(records)
211
+ ```
131
212
 
132
- 3. **Async Consumer Commit**:
133
- - **BREAKING**: The `consumer.commit()` method is now async and must be awaited
213
+ `producer.inFlightCount()` returns the number of messages sent but not yet acknowledged.
134
214
 
135
- </details>
215
+ ## Consumer API
136
216
 
137
- ---
217
+ ### Subscribe
138
218
 
139
- ## Features
219
+ The simplest subscription accepts a topic string:
140
220
 
141
- - 🦀 Simple and intuitive API
142
- - 🚀 High-performance message processing
143
- - 🔄 Automatic reconnection handling
144
- - 🎯 Type-safe interfaces (TypeScript support)
145
- - ⚡ Async/await support
146
- - 🛠️ Configurable consumer and producer options
147
- - 📊 Stream processing support with configurable stream options
148
- - 📦 Message batching capabilities
149
- - 🔍 Comprehensive error handling
150
- - 📈 Performance benchmarking suite
151
- - 🔧 Flexible configuration system supporting complex data types
221
+ ```ts
222
+ await consumer.subscribe('orders')
223
+ ```
152
224
 
153
- ## Table of Contents
225
+ Use `TopicPartitionConfig[]` when you need offsets, partition-specific offsets, or topic creation:
154
226
 
155
- 1. [Installation](#installation)
156
- 2. [Quick Start](#quick-start)
157
- 3. [Consumer Examples](#basic-consumer-setup)
158
- 4. [Producer Examples](#producer-examples)
159
- 5. [Stream Processing](#stream-processing)
160
- 6. [Configuration](#configuration)
161
- 7. [Performance Benchmarks](#performance-benchmarks)
162
- 8. [Best Practices](#best-practices)
163
- 9. [Contributing](#contributing)
164
- 10. [License](#license)
165
- 11. [OpenTelemetry Instrumentation](#opentelemetry-instrumentation)
227
+ ```ts
228
+ await consumer.subscribe([
229
+ {
230
+ topic: 'orders',
231
+ createTopic: true,
232
+ numPartitions: 3,
233
+ replicas: 1,
234
+ allOffsets: { position: 'Beginning' },
235
+ },
236
+ ])
237
+ ```
166
238
 
167
- ## Installation
239
+ For partition-specific offsets:
168
240
 
169
- ```bash
170
- npm install kafka-crab-js
171
- # or
172
- yarn add kafka-crab-js
173
- # or
174
- pnpm add kafka-crab-js
241
+ ```ts
242
+ await consumer.subscribe([
243
+ {
244
+ topic: 'orders',
245
+ partitionOffset: [
246
+ {
247
+ partition: 0,
248
+ offset: { offset: 42 },
249
+ },
250
+ ],
251
+ },
252
+ ])
175
253
  ```
176
254
 
177
- ## Quick Start
255
+ ### Receive
178
256
 
179
- ### Basic Consumer Setup
180
-
181
- ```javascript
182
- import { KafkaClient } from 'kafka-crab-js';
183
- async function run() {
184
- const kafkaClient = new KafkaClient({
185
- brokers: 'localhost:29092',
186
- clientId: 'foo-client',
187
- logLevel: 'debug',
188
- brokerAddressFamily: 'v4',
189
- });
190
-
191
- // Create consumer with topic creation control
192
- const consumer = kafkaClient.createConsumer({
193
- groupId: 'foo-group',
194
- configuration: {
195
- 'auto.offset.reset': 'earliest',
196
- 'enable.auto.commit': false, // Use manual commit for better control
197
- }
198
- });
257
+ `recv()` waits for one message and returns `null` when the consumer is disconnected:
199
258
 
200
- // Subscribe with topic creation options
201
- await consumer.subscribe([{
202
- topic: 'foo',
203
- createTopic: true,
204
- numPartitions: 3
205
- }]);
206
-
207
- const message = await consumer.recv();
208
- const { payload, partition, offset, topic } = message;
209
- console.log({
210
- topic,
211
- partition,
212
- offset,
213
- value: payload.toString()
214
- });
215
-
216
- // Manual commit - two options:
217
- // Option 1 (v2.1.0+): Simplified with commitMessage
218
- await consumer.commitMessage(message, 'Sync');
219
-
220
- // Option 2: Traditional commit with manual offset increment
221
- // await consumer.commit(topic, partition, offset + 1, 'Sync');
222
-
223
- consumer.unsubscribe();
259
+ ```ts
260
+ const message = await consumer.recv()
261
+ if (message) {
262
+ await handleMessage(message)
224
263
  }
264
+ ```
225
265
 
226
- await run();
266
+ `recvBatch(size, timeoutMs)` returns up to `size` messages:
267
+
268
+ ```ts
269
+ const messages = await consumer.recvBatch(1000, 100)
227
270
  ```
228
271
 
229
- ### Basic Producer Setup
272
+ An empty array means the timeout elapsed without a full batch. It does not mean the consumer is closed.
230
273
 
231
- ```javascript
232
- import { KafkaClient } from 'kafka-crab-js';
274
+ ### Commit
233
275
 
234
- const kafkaClient = new KafkaClient({
235
- brokers: 'localhost:29092',
236
- clientId: 'my-client-id',
237
- logLevel: 'info',
238
- brokerAddressFamily: 'v4',
239
- });
276
+ Use `commitMessage()` for normal message processing. It commits `message.offset + 1`, which is the offset Kafka expects:
240
277
 
241
- // Producer configuration is now optional with sensible defaults
242
- const producer = kafkaClient.createProducer({
243
- configuration: {
244
- 'message.timeout.ms': 5000, // Now supports number values
245
- 'batch.size': 16384,
246
- 'compression.type': 'snappy'
247
- }
248
- });
249
-
250
- const message = {
251
- id: 1,
252
- name: "Sample Message",
253
- timestamp: new Date().toISOString()
254
- };
255
-
256
- const result = await producer.send({
257
- topic: 'my-topic',
258
- messages: [{
259
- payload: Buffer.from(JSON.stringify(message))
260
- }]
261
- });
262
-
263
- const errors = result.map(r => r.error).filter(Boolean);
264
- if (errors.length > 0) {
265
- console.error('Error sending message:', errors);
266
- } else {
267
- console.log('Message sent. Offset:', result);
268
- }
278
+ ```ts
279
+ await consumer.commitMessage(message, 'Sync')
269
280
  ```
270
281
 
271
- ## Stream Processing
272
-
273
- ### Enhanced Stream Consumer Example
282
+ Use `commit()` when you already calculated the offset:
274
283
 
275
- ```javascript
276
- import { KafkaClient } from 'kafka-crab-js';
284
+ ```ts
285
+ await consumer.commit(message.topic, message.partition, message.offset + 1, 'Async')
286
+ ```
277
287
 
278
- const kafkaClient = new KafkaClient({
279
- brokers: 'localhost:29092',
280
- clientId: 'my-client-id',
281
- logLevel: 'info',
282
- brokerAddressFamily: 'v4',
283
- });
284
-
285
- // Stream consumer with custom ReadableOptions (v2.0.0+)
286
- const kafkaStream = kafkaClient.createStreamConsumer({
287
- groupId: `my-group-id`,
288
- enableAutoCommit: true,
289
- }, {
290
- objectMode: true, // Default in v2.0.0+
291
- highWaterMark: 1024,
292
- encoding: null
293
- });
294
-
295
- await kafkaStream.subscribe([
296
- { topic: 'foo', createTopic: true },
297
- { topic: 'bar', createTopic: true }
298
- ]);
299
-
300
- kafkaStream.on('data', (message) => {
301
- console.log('>>> Message received:', {
302
- payload: message.payload.toString(),
303
- offset: message.offset,
304
- partition: message.partition,
305
- topic: message.topic
306
- });
307
-
308
- if (message.offset > 10) {
309
- kafkaStream.destroy();
310
- }
311
- });
288
+ Commit modes are string literal values:
312
289
 
313
- kafkaStream.on('close', () => {
314
- kafkaStream.unsubscribe();
315
- console.log('Stream ended');
316
- });
290
+ ```ts
291
+ type CommitMode = 'Sync' | 'Async'
317
292
  ```
318
293
 
319
- ## Producer Examples
294
+ ### Pause, Resume, Seek, And Assignment
320
295
 
321
- ### Batch Message Production
296
+ ```ts
297
+ consumer.pause()
298
+ consumer.resume()
322
299
 
323
- ```javascript
324
- const kafkaClient = new KafkaClient({
325
- brokers: 'localhost:29092',
326
- clientId: 'my-client-id',
327
- brokerAddressFamily: 'v4',
328
- });
300
+ consumer.seek('orders', 0, { position: 'Beginning' })
301
+ consumer.seek('orders', 0, { offset: 42 })
329
302
 
330
- // Enhanced producer with flexible configuration
331
- const producer = kafkaClient.createProducer({
332
- configuration: {
333
- 'batch.size': 50000, // Number value supported
334
- 'linger.ms': 10, // Number value supported
335
- 'compression.type': 'lz4',
336
- 'enable.idempotence': true // Boolean value supported
303
+ const assignment = consumer.assignment()
304
+ const subscription = consumer.getSubscription()
305
+ ```
306
+
307
+ Consumer events are available through `onEvents()`:
308
+
309
+ ```ts
310
+ consumer.onEvents((error, event) => {
311
+ if (error) {
312
+ console.error(error)
313
+ return
337
314
  }
338
- });
339
315
 
340
- const messages = Array.from({ length: 100 }, (_, i) => ({
341
- payload: Buffer.from(JSON.stringify({
342
- _id: i,
343
- name: `Batch Message ${i}`,
344
- timestamp: new Date().toISOString()
345
- }))
346
- }));
316
+ console.log(event.name, event.payload)
317
+ })
318
+ ```
319
+
320
+ ### Cleanup
347
321
 
322
+ Always disconnect direct consumers:
323
+
324
+ ```ts
348
325
  try {
349
- const result = await producer.send({
350
- topic: 'my-topic',
351
- messages
352
- });
353
- console.log('Batch sent. Offset:', result);
354
- console.assert(result.length === 100);
355
- } catch (error) {
356
- console.error('Batch error:', error);
326
+ await consumer.subscribe('orders')
327
+ // consume...
328
+ } finally {
329
+ await consumer.disconnect()
357
330
  }
358
331
  ```
359
332
 
360
- ### Producer with Keys and Headers
333
+ ## Stream Consumers
361
334
 
362
- ```javascript
363
- async function produceWithMetadata() {
364
- const producer = kafkaClient.createProducer({
365
- configuration: {
366
- 'acks': 'all',
367
- 'retries': 5,
368
- 'max.in.flight.requests.per.connection': 1
369
- }
370
- });
335
+ ### Node.js Readable Stream
336
+
337
+ Use `createStreamConsumer()` when integrating with Node.js stream tooling. It emits `Message` objects.
338
+
339
+ ```ts
340
+ const stream = client.createStreamConsumer({
341
+ groupId: 'orders-stream',
342
+ enableAutoCommit: false,
343
+ batchSize: 256,
344
+ batchTimeout: 50,
345
+ configuration: {
346
+ 'auto.offset.reset': 'earliest',
347
+ },
348
+ })
371
349
 
350
+ await stream.subscribe('orders')
351
+
352
+ stream.on('data', async (message) => {
372
353
  try {
373
- await producer.send({
374
- topic: 'user-events',
375
- messages: [{
376
- key: 'user-123',
377
- payload: Buffer.from(JSON.stringify({
378
- userId: 123,
379
- action: 'update'
380
- })),
381
- headers: {
382
- 'correlation-id': 'txn-123',
383
- 'source': 'user-service'
384
- }
385
- }]
386
- });
354
+ await processOrder(message.payload)
355
+ await stream.commitMessage(message, 'Async')
387
356
  } catch (error) {
388
- console.error('Error:', error);
357
+ stream.destroy(error instanceof Error ? error : new Error(String(error)))
389
358
  }
390
- }
359
+ })
360
+
361
+ stream.on('error', (error) => {
362
+ console.error('stream error', error)
363
+ })
364
+
365
+ stream.on('close', () => {
366
+ console.log('stream closed')
367
+ })
391
368
  ```
392
369
 
393
- ### Reconnecting Kafka Consumer
370
+ Destroy stream consumers instead of calling `disconnect()` directly. The stream cleanup path cancels the source reader,
371
+ unsubscribes, and disconnects the native consumer:
394
372
 
395
- ```javascript
396
- import { KafkaClient } from 'kafka-crab-js'
373
+ ```ts
374
+ stream.destroy()
375
+ ```
397
376
 
398
- const kafkaClient = new KafkaClient({
399
- brokers: 'localhost:29092',
400
- clientId: 'reconnect-test',
401
- logLevel: 'debug',
402
- brokerAddressFamily: 'v4',
377
+ ### Native Web Stream
378
+
379
+ Use `createWebStreamConsumer()` when you want a Web Stream and clear serial/batch typing.
380
+
381
+ Serial mode is selected when `batchSize` is omitted, `0`, or `1`:
382
+
383
+ ```ts
384
+ const webConsumer = client.createWebStreamConsumer({
385
+ groupId: 'orders-web-serial',
386
+ serialPrefetchSize: 64,
387
+ serialPrefetchTimeout: 1,
403
388
  configuration: {
404
389
  'auto.offset.reset': 'earliest',
405
- 'session.timeout.ms': 30000,
406
- 'heartbeat.interval.ms': 10000
407
390
  },
408
391
  })
409
392
 
410
- /**
411
- * Creates and configures a new Kafka stream consumer
412
- */
413
- async function createConsumer() {
414
- const kafkaStream = kafkaClient.createStreamConsumer({
415
- groupId: 'reconnect-test',
416
- enableAutoCommit: true,
417
- }, {
418
- highWaterMark: 100,
419
- objectMode: true
420
- })
421
-
422
- await kafkaStream.subscribe([
423
- { topic: 'foo', createTopic: true },
424
- { topic: 'bar', createTopic: true },
425
- ])
426
- return kafkaStream
427
- }
393
+ await webConsumer.consumer.subscribe('orders')
428
394
 
429
- /**
430
- * Starts a Kafka consumer with auto-restart capability
431
- */
432
- async function startConsumer() {
433
- let counter = 0
434
- let retryCount = 0
435
- const MAX_RETRIES = 5
436
- const RETRY_DELAY = 5000 // 5 seconds
437
-
438
- async function handleRetry() {
439
- if (retryCount < MAX_RETRIES) {
440
- retryCount++
441
- console.log(
442
- `Attempting to restart consumer (attempt ${retryCount}/${MAX_RETRIES}) in ${RETRY_DELAY / 1000} seconds...`,
443
- )
444
- setTimeout(setupConsumerWithRetry, RETRY_DELAY)
445
- } else {
446
- console.error(`Maximum retry attempts (${MAX_RETRIES}) reached. Stopping consumer.`)
447
- process.exit(1)
448
- }
449
- }
395
+ const reader = webConsumer.stream.getReader()
450
396
 
451
- async function setupConsumerWithRetry() {
452
- try {
453
- const kafkaStream = await createConsumer()
454
- retryCount = 0 // Reset retry count on successful connection
455
-
456
- console.log('Starting consumer')
457
-
458
- kafkaStream.on('data', (message) => {
459
- counter++
460
- console.log('>>> Message received:', {
461
- counter,
462
- payload: message.payload.toString(),
463
- offset: message.offset,
464
- partition: message.partition,
465
- topic: message.topic,
466
- })
467
- })
468
-
469
- kafkaStream.on('error', async (error) => {
470
- console.error('Stream error:', error)
471
- handleRetry()
472
- })
473
-
474
- kafkaStream.on('close', () => {
475
- console.log('Stream ended')
476
- try {
477
- kafkaStream.unsubscribe()
478
- } catch (unsubError) {
479
- console.error('Error unsubscribing:', unsubError)
480
- }
481
- })
482
- } catch (error) {
483
- console.error('Error setting up consumer:', error)
484
- handleRetry()
485
- }
397
+ try {
398
+ const { value: message, done } = await reader.read()
399
+ if (!done && message) {
400
+ await processOrder(message.payload)
401
+ await webConsumer.consumer.commitMessage(message, 'Async')
486
402
  }
487
-
488
- await setupConsumerWithRetry()
403
+ } finally {
404
+ await reader.cancel()
405
+ await webConsumer.consumer.disconnect()
489
406
  }
490
-
491
- await startConsumer()
492
407
  ```
493
408
 
494
- ### Examples
409
+ Batch mode is selected with `batchSize > 1` and emits `Message[]` chunks:
410
+
411
+ ```ts
412
+ const webConsumer = client.createWebStreamConsumer({
413
+ groupId: 'orders-web-batch',
414
+ batchSize: 1024,
415
+ batchTimeout: 10,
416
+ configuration: {
417
+ 'auto.offset.reset': 'earliest',
418
+ },
419
+ })
495
420
 
496
- You can find some examples on the [example](https://github.com/flash-tecnologia/kafka-crab-js/tree/main/example) folder of this project.
421
+ if (webConsumer.mode === 'batch') {
422
+ await webConsumer.consumer.subscribe('orders')
423
+ const reader = webConsumer.stream.getReader()
424
+
425
+ try {
426
+ const { value: batch, done } = await reader.read()
427
+ if (!done && batch) {
428
+ for (const message of batch) {
429
+ await processOrder(message.payload)
430
+ }
431
+ }
432
+ } finally {
433
+ await reader.cancel()
434
+ await webConsumer.consumer.disconnect()
435
+ }
436
+ }
437
+ ```
497
438
 
498
439
  ## Configuration
499
440
 
500
- ### KafkaConfiguration
441
+ ### KafkaClient
442
+
443
+ ```ts
444
+ const client = new KafkaClient({
445
+ brokers: 'localhost:9092',
446
+ clientId: 'orders-service',
447
+ securityProtocol: 'Plaintext',
448
+ logLevel: 'info',
449
+ brokerAddressFamily: 'v4',
450
+ diagnostics: true,
451
+ configuration: {
452
+ 'socket.keepalive.enable': true,
453
+ },
454
+ })
455
+ ```
501
456
 
502
- | Property | Type | Default | Description |
503
- | --- | --- | --- | --- |
504
- | `brokers` | `string` || List of brokers to connect to |
505
- | `clientId` | `string` | `"rdkafka"` | Client id to use for the connection |
506
- | `securityProtocol` | `SecurityProtocol` || Security protocol to use (PLAINTEXT, SSL, SASL_PLAINTEXT, SASL_SSL) |
507
- | `logLevel` | `string` | `info` | Log level for the client |
508
- | `brokerAddressFamily` | `string` | `"v4"` | Address family to use for the connection (v4, v6) |
509
- | `configuration` | `Record<string, any>` | `{}` | Additional configuration options for the client |
510
- | `diagnostics` | `boolean` | `false` | **v3.0.0+**: Enable diagnostics channel for OTEL instrumentation |
457
+ | Option | Type | Default | Description |
458
+ | --------------------- | ------------------------------------------------------ | --------- | ------------------------------------------------------------------------- |
459
+ | `brokers` | `string` | required | Comma-separated broker list, for example `localhost:9092,localhost:9093`. |
460
+ | `clientId` | `string` | `rdkafka` | Client identifier sent to Kafka. |
461
+ | `securityProtocol` | `'Plaintext' \| 'Ssl' \| 'SaslPlaintext' \| 'SaslSsl'` | | Security protocol. |
462
+ | `logLevel` | `string` | | librdkafka log level. |
463
+ | `brokerAddressFamily` | `string` | | Address family hint such as `v4`. |
464
+ | `diagnostics` | `boolean` | `true` | Enables diagnostic-channel events used by `kafka-crab-js-otel`. |
465
+ | `configuration` | `Record<string, any>` | | Additional librdkafka client settings. |
511
466
 
512
467
  ### ConsumerConfiguration
513
468
 
514
- | Property | Type | Default | Description |
515
- | --- | --- | --- | --- |
516
- | `groupId` | `string` || Consumer group ID |
517
- | `enableAutoCommit` | `boolean` | `true` | Enable automatic offset commits |
518
- | `configuration` | `Record<string, any>` | `{}` | Additional consumer configuration options |
519
- | `fetchMetadataTimeout` | `number` | `60000` | Timeout for fetching metadata (ms) |
520
- | `maxBatchMessages` | `number` | `1000` | Maximum messages in a batch operation |
469
+ ```ts
470
+ const consumer = client.createConsumer({
471
+ groupId: 'orders-worker',
472
+ enableAutoCommit: false,
473
+ fetchMetadataTimeout: 5000,
474
+ configuration: {
475
+ 'auto.offset.reset': 'earliest',
476
+ 'enable.auto.commit': false,
477
+ 'fetch.min.bytes': 1,
478
+ },
479
+ })
480
+ ```
521
481
 
522
- ### Consumer Commit Methods
482
+ | Option | Type | Description |
483
+ | ---------------------- | --------------------- | ------------------------------------------ |
484
+ | `groupId` | `string` | Kafka consumer group id. |
485
+ | `enableAutoCommit` | `boolean` | Convenience flag for auto commit behavior. |
486
+ | `fetchMetadataTimeout` | `number` | Metadata fetch timeout in milliseconds. |
487
+ | `configuration` | `Record<string, any>` | Additional librdkafka consumer settings. |
523
488
 
524
- kafka-crab-js provides two methods for committing offsets:
489
+ ### ProducerConfiguration
525
490
 
526
- | Method | Signature | Description |
527
- | --- | --- | --- |
528
- | `commit` | `commit(topic, partition, offset, mode)` | Traditional commit - you must calculate `offset + 1` |
529
- | `commitMessage` | `commitMessage(message, mode)` | **v2.1.0+**: Simplified commit - automatically handles offset increment |
491
+ ```ts
492
+ const producer = client.createProducer({
493
+ autoFlush: true,
494
+ queueTimeout: 5000,
495
+ configuration: {
496
+ 'compression.type': 'lz4',
497
+ },
498
+ })
499
+ ```
530
500
 
531
- ```javascript
532
- // Using commitMessage (recommended for v2.1.0+)
533
- const message = await consumer.recv();
534
- await consumer.commitMessage(message, 'Sync');
501
+ | Option | Type | Description |
502
+ | --------------- | --------------------- | ------------------------------------------------------- |
503
+ | `autoFlush` | `boolean` | When enabled, `send()` waits for delivery confirmation. |
504
+ | `queueTimeout` | `number` | Queue timeout in milliseconds. |
505
+ | `configuration` | `Record<string, any>` | Additional librdkafka producer settings. |
535
506
 
536
- // Using commit (traditional)
537
- const message = await consumer.recv();
538
- await consumer.commit(message.topic, message.partition, message.offset + 1, 'Sync');
539
- ```
507
+ ### TopicPartitionConfig
540
508
 
541
- Both methods support `'Sync'` and `'Async'` commit modes.
509
+ | Option | Type | Description |
510
+ | ----------------- | ------------------- | ------------------------------------------------ |
511
+ | `topic` | `string` | Topic name. |
512
+ | `allOffsets` | `OffsetModel` | Offset model applied to all assigned partitions. |
513
+ | `partitionOffset` | `PartitionOffset[]` | Partition-specific offset model. |
514
+ | `createTopic` | `boolean` | Create the topic before subscribing. |
515
+ | `numPartitions` | `number` | Partition count when creating a topic. |
516
+ | `replicas` | `number` | Replica count when creating a topic. |
542
517
 
543
- ### ProducerConfiguration
518
+ Offset models:
544
519
 
545
- | Property | Type | Default | Description |
546
- | --- | --- | --- | --- |
547
- | `queueTimeout` | `number` | `5000` | Queue timeout in milliseconds |
548
- | `autoFlush` | `boolean` | `true` | Enable automatic message flushing |
549
- | `configuration` | `Record<string, any>` | `{}` | Additional producer configuration options |
520
+ ```ts
521
+ {
522
+ position: 'Beginning'
523
+ }
524
+ {
525
+ position: 'End'
526
+ }
527
+ {
528
+ position: 'Stored'
529
+ }
530
+ {
531
+ position: 'Invalid'
532
+ }
533
+ {
534
+ offset: 42
535
+ }
536
+ ```
550
537
 
551
- ### TopicPartitionConfig
538
+ ### Type-Only Literals
539
+
540
+ In v4, these names are TypeScript-only exports:
541
+
542
+ ```ts
543
+ import type { CommitMode, KafkaEventName, PartitionPosition, SecurityProtocol } from 'kafka-crab-js'
544
+ ```
552
545
 
553
- | Property | Type | Default | Description |
554
- | --- | --- | --- | --- |
555
- | `topic` | `string` || Topic name |
556
- | `allOffsets` | `OffsetModel` || Offset configuration for all partitions |
557
- | `partitionOffset` | `Array<PartitionOffset>` || Per-partition offset configuration |
558
- | `createTopic` | `boolean` | `false` | **v2.0.0+**: Create topic if it doesn't exist |
559
- | `numPartitions` | `number` | `1` | **v2.0.0+**: Number of partitions when creating topic |
560
- | `replicas` | `number` | `1` | **v2.0.0+**: Number of replicas when creating topic |
546
+ Use literal values at runtime:
561
547
 
562
- You can see the available options here: [librdkafka](https://docs.confluent.io/platform/current/clients/librdkafka/html/md_CONFIGURATION.html).
548
+ ```ts
549
+ const commitMode: CommitMode = 'Sync'
550
+ const securityProtocol: SecurityProtocol = 'Plaintext'
551
+ ```
563
552
 
564
- ### OpenTelemetry Instrumentation
553
+ ## OpenTelemetry
565
554
 
566
- > **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)
555
+ OpenTelemetry support lives in the separate `kafka-crab-js-otel` package. The core package emits diagnostic-channel
556
+ events when `diagnostics !== false`; the OTEL package subscribes to those events.
567
557
 
568
- #### Installation
558
+ Install the instrumentation package and OpenTelemetry dependencies:
569
559
 
570
560
  ```bash
571
- npm install kafka-crab-js-otel @opentelemetry/api
561
+ npm install kafka-crab-js-otel @opentelemetry/api @opentelemetry/sdk-node
572
562
  ```
573
563
 
574
- #### Usage
564
+ Enable instrumentation before creating `KafkaClient`:
575
565
 
576
- ```javascript
566
+ ```ts
577
567
  import { KafkaClient } from 'kafka-crab-js'
578
568
  import { enableOtelInstrumentation, endSpan } from 'kafka-crab-js-otel'
579
569
 
580
- // Enable instrumentation before creating client
581
- // Note: serviceName is configured via OTEL SDK Resource
582
570
  enableOtelInstrumentation({
583
571
  metrics: { enabled: true },
584
572
  })
585
573
 
586
- // Create client with diagnostics enabled
587
574
  const client = new KafkaClient({
588
575
  brokers: 'localhost:9092',
589
- clientId: 'my-app',
576
+ clientId: 'orders-worker',
590
577
  diagnostics: true,
591
578
  })
592
579
 
593
- // For consumers, call endSpan() when done processing
580
+ const consumer = client.createConsumer({ groupId: 'orders-worker' })
581
+ await consumer.subscribe('orders')
582
+
594
583
  const message = await consumer.recv()
595
- // ... process message ...
596
- endSpan(message)
584
+ if (message) {
585
+ try {
586
+ await processOrder(message.payload)
587
+ } finally {
588
+ endSpan(message)
589
+ }
590
+ }
597
591
  ```
598
592
 
599
- For complete configuration options and examples, see the [kafka-crab-js-otel README](../kafka-crab-js-otel/README.md).
600
-
601
- #### Examples
602
-
603
- See comprehensive examples in the `example/` directory:
604
- - `example/otel-tracing-example.mjs` - Complete tracing setup with Jaeger
605
- - `example/otel-metrics-example.mjs` - Metrics collection with Prometheus
606
- - `example/README.md` - Full documentation for all examples
593
+ See the [OpenTelemetry package README](../otel/README.md) for full tracing and metrics setup.
607
594
 
608
595
  ## Performance Benchmarks
609
596
 
610
- ### Running Benchmarks
597
+ The repository includes a benchmark suite that compares `kafka-crab-js` with KafkaJS and `@platformatic/kafka`.
611
598
 
612
- kafka-crab-js v2.0.0+ includes a comprehensive benchmark suite to compare performance against other popular Kafka clients:
599
+ From the repository root:
613
600
 
614
601
  ```bash
615
- # Set up benchmark environment (requires Kafka running locally)
616
- pnpm add -D tsx # For running TypeScript files directly
617
- npx tsx benchmark/utils/setup.ts
602
+ cd packages/benchmark
603
+
604
+ # Set up benchmark data. Requires Kafka running locally.
605
+ vp install
606
+ vp run setup:consumer
618
607
 
619
- # Run consumer performance benchmarks
620
- node benchmark/consumer.ts
608
+ # Run the default isolated-process memory benchmark.
609
+ vp run benchmark
621
610
  ```
622
611
 
623
- ### Benchmark Results
612
+ The default benchmark runs one child Node.js process per selected scenario. This avoids carrying V8 heap pages, native
613
+ allocator arenas, sockets, librdkafka state, and Kafka metadata from one client into the next client's measurement.
624
614
 
625
- *Benchmarks run on macOS with Apple M1 chip processing 50,000 messages (December 2024)*
615
+ ### Consumer Benchmark Snapshot
626
616
 
627
- ```
628
- ╔════════════════════════╤═════════╤══════════════════╤═══════════╤══════════════════════════╗
629
- ║ Slower tests │ Samples │ Result │ Tolerance │ Difference with previous ║
630
- ╟────────────────────────┼─────────┼──────────────────┼───────────┼──────────────────────────╢
631
- ║ kafkajs │ 50000 │ 834.12 op/sec │ ± 0.22 % │ ║
632
- ║ node-rdkafka (evented) │ 84115 │ 24922.67 op/sec │ ± 74.82 % │ + 2887.91 % ║
633
- ║ kafka-crab-js (serial) │ 50000 │ 43213.86 op/sec │ ± 3.46 % │ + 73.39 % ║
634
- ║ node-rdkafka (stream) │ 50000 │ 49805.32 op/sec │ ± 27.10 % │ + 15.25 % ║
635
- ╟────────────────────────┼─────────┼──────────────────┼───────────┼──────────────────────────╢
636
- ║ Fastest test │ Samples │ Result │ Tolerance │ Difference with previous ║
637
- ╟────────────────────────┼─────────┼──────────────────┼───────────┼──────────────────────────╢
638
- ║ kafka-crab-js (batch) │ 50000 │ 205985.31 op/sec │ ± 16.53 % │ + 313.58 % ║
639
- ╚════════════════════════╧═════════╧══════════════════╧═══════════╧══════════════════════════╝
617
+ The captured run below is equivalent to:
618
+
619
+ ```bash
620
+ cd packages/benchmark
621
+ BENCHMARK_BATCH_SIZE=300000 BENCHMARK_BATCH_TIMEOUT_MS=2 vp run benchmark
640
622
  ```
641
623
 
642
- The benchmark suite compares:
643
- - **kafka-crab-js (serial)**: Single message processing - **43,214 ops/sec**
644
- - **kafka-crab-js (batch)**: Batch message processing - **205,985 ops/sec** (fastest)
645
- - **node-rdkafka (evented)**: Event-based processing - **24,923 ops/sec**
646
- - **node-rdkafka (stream)**: Stream-based processing - **49,805 ops/sec**
647
- - **kafkajs**: Official KafkaJS client - **834 ops/sec**
624
+ Batch scenarios were normalized to the comparable effective batch size of `16384`.
648
625
 
649
- Performance characteristics:
650
- - **52x faster than kafkajs** in serial mode, **247x faster in batch mode**
651
- - **High throughput**: Batch processing provides 4.8x performance improvement over serial mode
652
- - **Low latency**: Optimized for both single and batch message processing
653
- - **Memory efficient**: Lock-free data structures minimize memory overhead
654
- - **Concurrent processing**: Zero-contention concurrent operations
626
+ ![Consumer benchmark snapshot](./assets/consumer-benchmark-snapshot.svg)
655
627
 
656
- ### Benchmark Configuration
628
+ #### Throughput
657
629
 
658
- You can customize benchmark parameters in `benchmark/utils/definitions.ts`:
630
+ | Rank | Scenario | Result | Relative |
631
+ | ---: | ----------------------------------- | --------------------: | -------: |
632
+ | 1 | `kafka-crab-js v4 (stream, batch)` | `1,089,284.97 op/sec` | `100.0%` |
633
+ | 2 | `@platformatic/kafka` | `732,207.27 op/sec` | `67.2%` |
634
+ | 3 | `KafkaJS (eachBatch)` | `676,576.98 op/sec` | `62.1%` |
635
+ | 4 | `kafka-crab-js v4 (stream, serial)` | `544,658.33 op/sec` | `50.0%` |
636
+ | 5 | `KafkaJS (eachMessage, concurrent)` | `509,082.43 op/sec` | `46.7%` |
637
+ | 6 | `KafkaJS (eachMessage)` | `505,815.57 op/sec` | `46.4%` |
659
638
 
660
- ```typescript
661
- export const topic = 'benchmarks'
662
- export const brokers = ['localhost:9092', 'localhost:9093', 'localhost:9094']
639
+ `kafka-crab-js v4 (stream, batch)` is the fastest scenario in this run. It is about `49%` faster than
640
+ `@platformatic/kafka` and about `61%` faster than `KafkaJS (eachBatch)`. Its tolerance is higher than the other top
641
+ rows (`+/- 9.15%`), so the exact gap should be treated as a benchmark snapshot rather than a universal constant.
663
642
 
664
- // Benchmark parameters can be adjusted in consumer.ts:
665
- const iterations = 10_000 // Number of messages to process
666
- const maxBytes = 200 // Maximum message size
667
- ```
643
+ For message-by-message consumption, `kafka-crab-js v4 (stream, serial)` is close to KafkaJS throughput while using much
644
+ less lifecycle memory. In this run it is about `7.7%` faster than `KafkaJS (eachMessage)` and about `7.0%` faster than
645
+ the concurrent KafkaJS `eachMessage` scenario.
668
646
 
669
- ## Best Practices
647
+ #### Memory And GC
670
648
 
671
- ### Error Handling
672
- - Always wrap async operations in try-catch blocks
673
- - Implement proper error logging and monitoring
674
- - Handle both operational and programming errors separately
649
+ | Scenario | RSS delta | Peak heap | GC time | GC share | Notes |
650
+ | ----------------------------------- | ----------: | ---------: | ---------: | -------: | ----------------------------------------------- |
651
+ | `kafka-crab-js v4 (stream, serial)` | `72.3 MiB` | `11.7 MiB` | `36.75 ms` | `4.00%` | Best memory efficiency. |
652
+ | `kafka-crab-js v4 (stream, batch)` | `167.0 MiB` | `35.9 MiB` | `30.60 ms` | `6.67%` | Best throughput and lowest GC time. |
653
+ | `KafkaJS (eachBatch)` | `193.1 MiB` | `70.0 MiB` | `63.03 ms` | `8.53%` | Strong batch baseline. |
654
+ | `@platformatic/kafka` | `217.6 MiB` | `93.7 MiB` | `73.62 ms` | `10.78%` | Fastest non-crab competitor. |
655
+ | `KafkaJS (eachMessage)` | `181.5 MiB` | `77.0 MiB` | `79.11 ms` | `8.00%` | Similar throughput to v4 serial, higher memory. |
656
+ | `KafkaJS (eachMessage, concurrent)` | `188.2 MiB` | `75.6 MiB` | `77.17 ms` | `7.86%` | Concurrency did not improve this workload. |
675
657
 
676
- ### Performance
677
- - Use batch operations for high-throughput scenarios
678
- - Configure appropriate batch sizes and compression settings
679
- - Monitor and tune consumer group performance
680
- - Leverage the benchmark suite to optimize your specific use case
658
+ Lifecycle memory includes module loading, client creation, subscription, consumption, cleanup, and retained RSS after
659
+ cleanup. GC metrics use the narrower first-to-last-message window used for throughput.
681
660
 
682
- ### Configuration (v2.0.0+)
683
- - Use the flexible configuration system with proper data types:
684
- ```javascript
685
- const config = {
686
- 'batch.size': 16384, // number
687
- 'compression.type': 'snappy', // string
688
- 'enable.idempotence': true, // boolean
689
- 'retries': 5 // number
690
- }
691
- ```
692
-
693
- ### Message Processing
694
- - Validate message formats before processing
695
- - Implement proper serialization/deserialization
696
- - Handle message ordering when required
697
- - Use topic creation options for better topic management
661
+ The most important memory result is efficiency, not just raw RSS. `kafka-crab-js v4 (stream, serial)` delivered
662
+ `7529 op/sec/MiB`, the best score in the run. `kafka-crab-js v4 (stream, batch)` delivered `6521 op/sec/MiB`, ahead of
663
+ `KafkaJS (eachBatch)` at `3504 op/sec/MiB` and `@platformatic/kafka` at `3365 op/sec/MiB`.
698
664
 
699
- ### Stream Processing (v2.0.0+)
700
- - Configure appropriate `ReadableOptions` for your use case
701
- - Use `objectMode: true` for structured message processing
702
- - Set appropriate `highWaterMark` based on memory constraints
665
+ ### Benchmark Interpretation
703
666
 
704
- ## Contributing
667
+ - Use `kafka-crab-js v4 (stream, batch)` when raw throughput is the priority.
668
+ - Use `kafka-crab-js v4 (stream, serial)` when memory efficiency and low heap pressure matter most.
669
+ - Compare batch scenarios with batch scenarios and message scenarios with message scenarios.
670
+ - `@platformatic/kafka` is the strongest non-crab throughput competitor in this run, but it used more RSS, heap, and GC
671
+ time than the v4 batch scenario.
672
+ - KafkaJS `eachMessage` concurrency did not help this workload.
705
673
 
706
- 1. Fork the repository
707
- 2. Create your feature branch (`git checkout -b feature/amazing-feature`)
708
- 3. Run the benchmark suite to ensure performance isn't degraded
709
- 4. Commit your changes (`git commit -m 'Add some amazing feature'`)
710
- 5. Push to the branch (`git push origin feature/amazing-feature`)
711
- 6. Open a Pull Request
674
+ ### Benchmark Configuration
712
675
 
713
- ### Development Commands
676
+ Common knobs:
714
677
 
715
678
  ```bash
716
- # Build the project
717
- pnpm build
679
+ BENCHMARK_ITERATIONS=100000
680
+ BENCHMARK_RUNS=5
681
+ BENCHMARK_BATCH_SIZE=4096
682
+ BENCHMARK_BATCH_TIMEOUT_MS=2
683
+ BENCHMARK_MAX_BYTES=2048
684
+ BENCHMARK_MEMORY=1
685
+ ```
686
+
687
+ See the [benchmark README](../benchmark/README.md) for the full benchmark methodology and environment variables.
718
688
 
719
- # Run type checking
720
- pnpm typecheck
689
+ ## Migration Notes
721
690
 
722
- # Run linting
723
- pnpm lint
691
+ ### v4 Type-Only Runtime Exports
724
692
 
725
- # Format code
726
- pnpm fmt
693
+ `CommitMode`, `KafkaEventName`, `PartitionPosition`, and `SecurityProtocol` are no longer runtime exports from
694
+ `kafka-crab-js`.
727
695
 
728
- # Run benchmarks
729
- npx tsx benchmark/utils/setup.ts
730
- npx tsx benchmark/consumer.ts
696
+ Before:
697
+
698
+ ```ts
699
+ import { CommitMode, SecurityProtocol } from 'kafka-crab-js'
731
700
  ```
732
701
 
733
- ## OpenTelemetry Instrumentation
702
+ After:
734
703
 
735
- > **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)
704
+ ```ts
705
+ import type { CommitMode, SecurityProtocol } from 'kafka-crab-js'
736
706
 
737
- Kafka Crab JS offers turnkey tracing for Kafka workloads:
707
+ const commitMode: CommitMode = 'Sync'
708
+ const securityProtocol: SecurityProtocol = 'Plaintext'
709
+ ```
738
710
 
739
- - **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.
740
- - **Consumer & stream coverage** – Standard consumers, batch consumers, and `createStreamConsumer` streams emit spans that include consumer group, topic, partition, offset, and batch size semantics.
741
- - **Hook-friendly spans** – Both `messageHook` and `producerHook` callbacks run inside the active span context, simplifying attribute decoration or error handling.
742
- - **Zero overhead when disabled** – Uses Node.js `diagnostics_channel` for near-zero cost when OTEL is not active.
711
+ ### v3 OpenTelemetry Split
743
712
 
744
- ### Consumer span lifecycle (important)
713
+ OpenTelemetry instrumentation moved from the core package to `kafka-crab-js-otel` in v3. This keeps the core package
714
+ small and makes tracing/metrics opt-in.
745
715
 
746
- Kafka Crab JS creates consumer `process <topic>` spans, but only your application knows when processing is complete.
716
+ ## Troubleshooting
747
717
 
748
- - For single-message consumers, call `endSpan(message)` when you're done processing the message.
749
- - For batch consumers, call `endSpan(batch)` when you're done processing the batch.
750
- - The `endSpan()` helper is exported from `kafka-crab-js-otel`.
718
+ ### The Consumer Does Not Receive Existing Messages
751
719
 
752
- This closes the span(s) and (when metrics are enabled) records `messaging.process.duration`.
720
+ Use an offset reset policy and a fresh consumer group:
753
721
 
754
- ### Global instrumentation (singleton)
722
+ ```ts
723
+ const consumer = client.createConsumer({
724
+ groupId: `orders-worker-${Date.now()}`,
725
+ configuration: {
726
+ 'auto.offset.reset': 'earliest',
727
+ },
728
+ })
729
+ ```
755
730
 
756
- `kafka-crab-js-otel` uses a process-wide OpenTelemetry instrumentation singleton.
731
+ Or subscribe with an explicit starting position:
757
732
 
758
- - Call `enableOtelInstrumentation()` **before** creating any `KafkaClient` instances.
759
- - Creating multiple `KafkaClient` instances shares the same instrumentation.
760
- - For tests, use `resetKafkaInstrumentation()` to clear the singleton between runs.
733
+ ```ts
734
+ await consumer.subscribe([{ topic: 'orders', allOffsets: { position: 'Beginning' } }])
735
+ ```
761
736
 
762
- ### Minimal Setup Example
737
+ ### The Process Hangs On Shutdown
763
738
 
764
- When using stream consumers, the easiest pattern is calling `endSpan()` in a `finally` block:
739
+ Direct consumers should call `disconnect()`:
765
740
 
766
741
  ```ts
767
- import { KafkaClient } from 'kafka-crab-js'
768
- import { enableOtelInstrumentation, endSpan } from 'kafka-crab-js-otel'
769
- import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node'
770
- import { SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base'
771
- import { AsyncHooksContextManager } from '@opentelemetry/context-async-hooks'
772
- import { context } from '@opentelemetry/api'
773
-
774
- // 1. Set up OpenTelemetry SDK
775
- const provider = new NodeTracerProvider()
776
- provider.addSpanProcessor(new SimpleSpanProcessor(exporter))
777
- provider.register()
778
- context.setGlobalContextManager(new AsyncHooksContextManager().enable())
779
-
780
- // 2. Enable kafka-crab-js instrumentation BEFORE creating client
781
- enableOtelInstrumentation({
782
- captureMessagePayload: true,
783
- captureMessageHeaders: true,
784
- producerHook: span => span.setAttribute('messaging.client.kind', 'producer'),
785
- })
742
+ await consumer.disconnect()
743
+ ```
786
744
 
787
- // 3. Create client with diagnostics enabled
788
- const client = new KafkaClient({
789
- brokers: 'localhost:29092',
790
- clientId: 'orders-api',
791
- diagnostics: true, // Required for OTEL to receive events
792
- })
745
+ Node stream consumers should call `destroy()`:
793
746
 
794
- // 4. Use producer - spans are created automatically
795
- const producer = client.createProducer()
796
- await producer.send({
797
- topic: 'orders',
798
- messages: [{
799
- payload: Buffer.from(JSON.stringify({ orderId: '123' })),
800
- headers: { 'custom-header': 'foo' },
801
- }],
802
- })
747
+ ```ts
748
+ stream.destroy()
749
+ ```
803
750
 
804
- // 5. Use stream consumer with proper cleanup
805
- const consumer = client.createStreamConsumer({
806
- groupId: 'orders-consumer',
807
- enableAutoCommit: false,
808
- })
751
+ Web stream consumers should cancel the reader and disconnect the underlying consumer:
809
752
 
810
- await consumer.subscribe('orders')
753
+ ```ts
754
+ await reader.cancel()
755
+ await webConsumer.consumer.disconnect()
756
+ ```
811
757
 
812
- consumer.on('data', message => {
813
- try {
814
- console.log(message.headers?.['custom-header']?.toString())
815
- } finally {
816
- // IMPORTANT: close the "process <topic>" span when your app finishes handling the message
817
- endSpan(message)
818
- }
758
+ ### Localhost Resolves To IPv6 But Kafka Listens On IPv4
759
+
760
+ Set `brokerAddressFamily: 'v4'`:
761
+
762
+ ```ts
763
+ const client = new KafkaClient({
764
+ brokers: 'localhost:9092',
765
+ brokerAddressFamily: 'v4',
819
766
  })
767
+ ```
768
+
769
+ ### Need Advanced Kafka Settings
820
770
 
821
- // Proper cleanup for stream consumers - use destroy() not disconnect()
822
- consumer.on('close', () => console.log('Consumer closed'))
823
- // consumer.destroy()
771
+ Pass librdkafka settings through `configuration`:
772
+
773
+ ```ts
774
+ const client = new KafkaClient({
775
+ brokers: 'localhost:9092',
776
+ configuration: {
777
+ 'socket.keepalive.enable': true,
778
+ 'metadata.max.age.ms': 300000,
779
+ },
780
+ })
824
781
  ```
825
782
 
826
- To disable OTEL, simply don't call `enableOtelInstrumentation()` or set `diagnostics: false` in the client config.
783
+ Consumer and producer configuration objects also accept their own `configuration` maps.
827
784
 
828
- ## License
785
+ ## Development
829
786
 
830
- This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
787
+ From the repository root:
831
788
 
832
- ---
789
+ ```bash
790
+ vp install
791
+ vp run build
792
+ vp run test
793
+ vp run test:integration
794
+ vp check
795
+ ```
833
796
 
834
- <div align="center">
797
+ Useful package-local commands:
835
798
 
836
- [![Built with Rust](https://img.shields.io/badge/Built%20with-Rust-orange)](https://www.rust-lang.org/)
799
+ ```bash
800
+ vp run --filter kafka-crab-js build
801
+ vp run --filter kafka-crab-js test
802
+ vp run --filter kafka-crab-js test:integration
803
+ ```
804
+
805
+ Integration tests require Kafka at `localhost:9092` unless `KAFKA_BROKERS` is set.
806
+
807
+ ## License
837
808
 
838
- </div>
809
+ MIT