kafka-crab-js 4.0.0-beta.3 → 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 +378 -56
- package/assets/consumer-benchmark-snapshot.svg +82 -73
- package/dist/diagnostics/channels.cjs.map +1 -1
- package/dist/diagnostics/channels.js.map +1 -1
- package/dist/diagnostics/instrumentation.cjs.map +1 -1
- package/dist/diagnostics/instrumentation.js.map +1 -1
- package/dist/kafka-client.cjs.map +1 -1
- package/dist/kafka-client.js.map +1 -1
- package/dist/streams/base-kafka-stream-readable.cjs.map +1 -1
- package/dist/streams/base-kafka-stream-readable.js.map +1 -1
- package/dist/streams/kafka-batch-stream-readable.cjs.map +1 -1
- package/dist/streams/kafka-batch-stream-readable.js.map +1 -1
- package/dist/streams/kafka-stream-readable.cjs.map +1 -1
- package/dist/streams/kafka-stream-readable.js.map +1 -1
- package/js-binding.cjs +52 -52
- package/js-binding.js +52 -52
- package/package.json +10 -9
package/README.md
CHANGED
|
@@ -1,16 +1,57 @@
|
|
|
1
1
|
# Kafka Crab JS
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
JavaScript
|
|
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.
|
|
5
6
|
|
|
6
|
-
[](https://www.npmjs.com/package/kafka-crab-js)
|
|
7
8
|
[](https://opensource.org/licenses/MIT)
|
|
8
9
|
|
|
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
|
+
|
|
9
49
|
## Highlights
|
|
10
50
|
|
|
11
51
|
- Native librdkafka client exposed through a TypeScript-friendly API.
|
|
12
|
-
- Producer, direct consumer, Node.js `Readable` stream, and native Web Stream consumer APIs.
|
|
52
|
+
- Producer, direct consumer, Node.js `Readable` stream, and v4 native Web Stream consumer APIs.
|
|
13
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.
|
|
14
55
|
- Manual commit helpers that commit `message.offset + 1` correctly.
|
|
15
56
|
- Optional diagnostics-channel instrumentation for OpenTelemetry through `kafka-crab-js-otel`.
|
|
16
57
|
- Advanced librdkafka settings are forwarded through `configuration` without forcing a custom allowlist.
|
|
@@ -23,19 +64,23 @@ JavaScript API layer.
|
|
|
23
64
|
|
|
24
65
|
## Table Of Contents
|
|
25
66
|
|
|
26
|
-
1. [
|
|
27
|
-
2. [
|
|
28
|
-
3. [
|
|
29
|
-
4. [
|
|
30
|
-
5. [
|
|
31
|
-
6. [
|
|
32
|
-
7. [
|
|
33
|
-
8. [
|
|
34
|
-
9. [
|
|
35
|
-
10. [
|
|
36
|
-
11. [
|
|
37
|
-
12. [
|
|
38
|
-
13. [
|
|
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)
|
|
39
84
|
|
|
40
85
|
## Installation
|
|
41
86
|
|
|
@@ -51,6 +96,31 @@ pnpm add kafka-crab-js
|
|
|
51
96
|
yarn add kafka-crab-js
|
|
52
97
|
```
|
|
53
98
|
|
|
99
|
+
## Module Usage
|
|
100
|
+
|
|
101
|
+
The package publishes both ESM and CommonJS entry points:
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
import { KafkaClient } from 'kafka-crab-js'
|
|
105
|
+
import type { Message, ProducerRecord } from 'kafka-crab-js'
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
```js
|
|
109
|
+
const { KafkaClient } = require('kafka-crab-js')
|
|
110
|
+
```
|
|
111
|
+
|
|
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:
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
await consumer.commitMessage(message, 'Sync')
|
|
117
|
+
|
|
118
|
+
const client = new KafkaClient({
|
|
119
|
+
brokers: 'localhost:9092',
|
|
120
|
+
securityProtocol: 'Plaintext',
|
|
121
|
+
})
|
|
122
|
+
```
|
|
123
|
+
|
|
54
124
|
## Quick Start
|
|
55
125
|
|
|
56
126
|
### Produce Messages
|
|
@@ -150,6 +220,45 @@ try {
|
|
|
150
220
|
}
|
|
151
221
|
```
|
|
152
222
|
|
|
223
|
+
## Message Model
|
|
224
|
+
|
|
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:
|
|
227
|
+
|
|
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
|
+
}
|
|
239
|
+
|
|
240
|
+
function decodeJsonMessage(message: Message) {
|
|
241
|
+
return JSON.parse(message.payload.toString('utf8')) as unknown
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function logDelivery(records: RecordMetadata[]) {
|
|
245
|
+
for (const record of records) {
|
|
246
|
+
console.log(`${record.topic}[${record.partition}]@${record.offset}`)
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
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:
|
|
253
|
+
|
|
254
|
+
```ts
|
|
255
|
+
await consumer.commitMessage(message, 'Sync')
|
|
256
|
+
await consumer.commit(message.topic, message.partition, message.offset + 1, 'Async')
|
|
257
|
+
```
|
|
258
|
+
|
|
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.
|
|
261
|
+
|
|
153
262
|
## Choosing A Consumer API
|
|
154
263
|
|
|
155
264
|
| API | Emits | Best For |
|
|
@@ -168,6 +277,13 @@ Two details matter for choosing correctly:
|
|
|
168
277
|
- `createWebStreamConsumer()` returns a discriminated object. In serial mode it emits `Message`; in batch mode it emits
|
|
169
278
|
`Message[]`. Use this API when your code can process whole batches directly.
|
|
170
279
|
|
|
280
|
+
Recommended starting points:
|
|
281
|
+
|
|
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.
|
|
286
|
+
|
|
171
287
|
## Producer API
|
|
172
288
|
|
|
173
289
|
Create a producer from a `KafkaClient`:
|
|
@@ -212,6 +328,51 @@ console.log(records)
|
|
|
212
328
|
|
|
213
329
|
`producer.inFlightCount()` returns the number of messages sent but not yet acknowledged.
|
|
214
330
|
|
|
331
|
+
### Delivery Semantics
|
|
332
|
+
|
|
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:
|
|
336
|
+
|
|
337
|
+
```ts
|
|
338
|
+
const records = await producer.send({
|
|
339
|
+
topic: 'orders',
|
|
340
|
+
messages: [{ payload: Buffer.from('created') }],
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
for (const record of records) {
|
|
344
|
+
if (record.error) {
|
|
345
|
+
throw new Error(`Kafka delivery failed: ${record.error.message}`)
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
```
|
|
349
|
+
|
|
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:
|
|
352
|
+
|
|
353
|
+
```ts
|
|
354
|
+
import type { RecordMetadata } from 'kafka-crab-js'
|
|
355
|
+
|
|
356
|
+
const producer = client.createProducer({ autoFlush: false })
|
|
357
|
+
|
|
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
|
+
}
|
|
365
|
+
|
|
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)
|
|
373
|
+
}
|
|
374
|
+
```
|
|
375
|
+
|
|
215
376
|
## Consumer API
|
|
216
377
|
|
|
217
378
|
### Subscribe
|
|
@@ -291,6 +452,25 @@ Commit modes are string literal values:
|
|
|
291
452
|
type CommitMode = 'Sync' | 'Async'
|
|
292
453
|
```
|
|
293
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,
|
|
462
|
+
})
|
|
463
|
+
|
|
464
|
+
const message = await consumer.recv()
|
|
465
|
+
if (message) {
|
|
466
|
+
await processOrder(message)
|
|
467
|
+
await consumer.commitMessage(message, 'Sync')
|
|
468
|
+
}
|
|
469
|
+
```
|
|
470
|
+
|
|
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.
|
|
473
|
+
|
|
294
474
|
### Pause, Resume, Seek, And Assignment
|
|
295
475
|
|
|
296
476
|
```ts
|
|
@@ -317,6 +497,9 @@ consumer.onEvents((error, event) => {
|
|
|
317
497
|
})
|
|
318
498
|
```
|
|
319
499
|
|
|
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.
|
|
502
|
+
|
|
320
503
|
### Cleanup
|
|
321
504
|
|
|
322
505
|
Always disconnect direct consumers:
|
|
@@ -436,6 +619,46 @@ if (webConsumer.mode === 'batch') {
|
|
|
436
619
|
}
|
|
437
620
|
```
|
|
438
621
|
|
|
622
|
+
## Batching, Backpressure, And Tuning
|
|
623
|
+
|
|
624
|
+
The main throughput lever is how many messages cross the native-to-JavaScript boundary per call.
|
|
625
|
+
|
|
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',
|
|
649
|
+
configuration: {
|
|
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,
|
|
655
|
+
},
|
|
656
|
+
})
|
|
657
|
+
```
|
|
658
|
+
|
|
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.
|
|
661
|
+
|
|
439
662
|
## Configuration
|
|
440
663
|
|
|
441
664
|
### KafkaClient
|
|
@@ -459,11 +682,38 @@ const client = new KafkaClient({
|
|
|
459
682
|
| `brokers` | `string` | required | Comma-separated broker list, for example `localhost:9092,localhost:9093`. |
|
|
460
683
|
| `clientId` | `string` | `rdkafka` | Client identifier sent to Kafka. |
|
|
461
684
|
| `securityProtocol` | `'Plaintext' \| 'Ssl' \| 'SaslPlaintext' \| 'SaslSsl'` | | Security protocol. |
|
|
462
|
-
| `logLevel` | `string` |
|
|
463
|
-
| `brokerAddressFamily` | `string` |
|
|
685
|
+
| `logLevel` | `string` | `error` | librdkafka log level. |
|
|
686
|
+
| `brokerAddressFamily` | `string` | `v4` | Address family hint such as `v4` or `any`. |
|
|
464
687
|
| `diagnostics` | `boolean` | `true` | Enables diagnostic-channel events used by `kafka-crab-js-otel`. |
|
|
465
688
|
| `configuration` | `Record<string, any>` | | Additional librdkafka client settings. |
|
|
466
689
|
|
|
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`.
|
|
692
|
+
|
|
693
|
+
Common connection examples:
|
|
694
|
+
|
|
695
|
+
```ts
|
|
696
|
+
const localClient = new KafkaClient({
|
|
697
|
+
brokers: 'localhost:9092',
|
|
698
|
+
clientId: 'orders-local',
|
|
699
|
+
securityProtocol: 'Plaintext',
|
|
700
|
+
brokerAddressFamily: 'v4',
|
|
701
|
+
})
|
|
702
|
+
```
|
|
703
|
+
|
|
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
|
+
```
|
|
716
|
+
|
|
467
717
|
### ConsumerConfiguration
|
|
468
718
|
|
|
469
719
|
```ts
|
|
@@ -486,6 +736,22 @@ const consumer = client.createConsumer({
|
|
|
486
736
|
| `fetchMetadataTimeout` | `number` | Metadata fetch timeout in milliseconds. |
|
|
487
737
|
| `configuration` | `Record<string, any>` | Additional librdkafka consumer settings. |
|
|
488
738
|
|
|
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.
|
|
741
|
+
|
|
742
|
+
For new services that need explicit processing guarantees, start with manual commits:
|
|
743
|
+
|
|
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
|
+
```
|
|
754
|
+
|
|
489
755
|
### ProducerConfiguration
|
|
490
756
|
|
|
491
757
|
```ts
|
|
@@ -504,6 +770,22 @@ const producer = client.createProducer({
|
|
|
504
770
|
| `queueTimeout` | `number` | Queue timeout in milliseconds. |
|
|
505
771
|
| `configuration` | `Record<string, any>` | Additional librdkafka producer settings. |
|
|
506
772
|
|
|
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:
|
|
775
|
+
|
|
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
|
+
```
|
|
788
|
+
|
|
507
789
|
### TopicPartitionConfig
|
|
508
790
|
|
|
509
791
|
| Option | Type | Description |
|
|
@@ -590,23 +872,26 @@ if (message) {
|
|
|
590
872
|
}
|
|
591
873
|
```
|
|
592
874
|
|
|
593
|
-
See the [OpenTelemetry package README](../otel/README.md) for full tracing and metrics setup.
|
|
875
|
+
See the [OpenTelemetry package README](../kafka-crab-js-otel/README.md) for full tracing and metrics setup.
|
|
594
876
|
|
|
595
877
|
## Performance Benchmarks
|
|
596
878
|
|
|
597
|
-
The repository includes a benchmark suite that compares `kafka-crab-js` with KafkaJS and `@platformatic/kafka`.
|
|
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.
|
|
598
882
|
|
|
599
883
|
From the repository root:
|
|
600
884
|
|
|
601
885
|
```bash
|
|
602
|
-
|
|
886
|
+
podman compose up -d
|
|
887
|
+
|
|
888
|
+
cd benchmarks/kafka
|
|
603
889
|
|
|
604
890
|
# Set up benchmark data. Requires Kafka running locally.
|
|
605
|
-
vp
|
|
606
|
-
vp run setup:consumer
|
|
891
|
+
KAFKA_BROKERS=127.0.0.1:9092,127.0.0.1:9093,127.0.0.1:9094 vp run setup:consumer
|
|
607
892
|
|
|
608
893
|
# Run the default isolated-process memory benchmark.
|
|
609
|
-
vp run benchmark
|
|
894
|
+
KAFKA_BROKERS=127.0.0.1:9092,127.0.0.1:9093,127.0.0.1:9094 vp run benchmark
|
|
610
895
|
```
|
|
611
896
|
|
|
612
897
|
The default benchmark runs one child Node.js process per selected scenario. This avoids carrying V8 heap pages, native
|
|
@@ -617,11 +902,11 @@ allocator arenas, sockets, librdkafka state, and Kafka metadata from one client
|
|
|
617
902
|
The captured run below is equivalent to:
|
|
618
903
|
|
|
619
904
|
```bash
|
|
620
|
-
cd
|
|
621
|
-
|
|
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
|
|
622
907
|
```
|
|
623
908
|
|
|
624
|
-
|
|
909
|
+
The run uses the default isolated-process lifecycle-memory benchmark mode.
|
|
625
910
|
|
|
626
911
|

|
|
627
912
|
|
|
@@ -629,62 +914,71 @@ Batch scenarios were normalized to the comparable effective batch size of `16384
|
|
|
629
914
|
|
|
630
915
|
| Rank | Scenario | Result | Relative |
|
|
631
916
|
| ---: | ----------------------------------- | --------------------: | -------: |
|
|
632
|
-
| 1 | `kafka-crab-js v4 (stream, batch)` | `1,
|
|
633
|
-
| 2 | `@platformatic/kafka` | `
|
|
634
|
-
| 3 | `
|
|
635
|
-
| 4 | `
|
|
636
|
-
| 5 | `KafkaJS (eachMessage
|
|
637
|
-
| 6 | `KafkaJS (eachMessage)`
|
|
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%` |
|
|
638
923
|
|
|
639
|
-
`kafka-crab-js v4 (stream, batch)` is the fastest scenario in this run. It is about `
|
|
640
|
-
`@platformatic/kafka` and about `
|
|
641
|
-
|
|
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.
|
|
642
927
|
|
|
643
928
|
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 `
|
|
645
|
-
the concurrent KafkaJS `eachMessage` scenario.
|
|
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.
|
|
646
931
|
|
|
647
932
|
#### Memory And GC
|
|
648
933
|
|
|
649
|
-
| Scenario | RSS delta | Peak heap | GC time | GC share | Notes
|
|
650
|
-
| ----------------------------------- | ----------: | ---------: | ---------: | -------: |
|
|
651
|
-
| `kafka-crab-js v4 (stream, serial)` | `
|
|
652
|
-
| `kafka-crab-js v4 (stream, batch)` | `
|
|
653
|
-
| `KafkaJS (
|
|
654
|
-
|
|
|
655
|
-
| `KafkaJS (
|
|
656
|
-
| `
|
|
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. |
|
|
657
942
|
|
|
658
943
|
Lifecycle memory includes module loading, client creation, subscription, consumption, cleanup, and retained RSS after
|
|
659
944
|
cleanup. GC metrics use the narrower first-to-last-message window used for throughput.
|
|
660
945
|
|
|
661
|
-
The most important memory result is efficiency, not just raw RSS. `kafka-crab-js v4 (stream,
|
|
662
|
-
`
|
|
663
|
-
|
|
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`.
|
|
664
949
|
|
|
665
950
|
### Benchmark Interpretation
|
|
666
951
|
|
|
667
|
-
- Use `kafka-crab-js v4 (stream, batch)` when raw throughput is the priority.
|
|
668
|
-
- Use `kafka-crab-js v4 (stream, serial)` when
|
|
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.
|
|
669
955
|
- Compare batch scenarios with batch scenarios and message scenarios with message scenarios.
|
|
670
956
|
- `@platformatic/kafka` is the strongest non-crab throughput competitor in this run, but it used more RSS, heap, and GC
|
|
671
|
-
time than
|
|
957
|
+
time than both v4 scenarios.
|
|
672
958
|
- KafkaJS `eachMessage` concurrency did not help this workload.
|
|
673
959
|
|
|
674
960
|
### Benchmark Configuration
|
|
675
961
|
|
|
676
|
-
|
|
962
|
+
Captured benchmark settings:
|
|
677
963
|
|
|
678
964
|
```bash
|
|
679
965
|
BENCHMARK_ITERATIONS=100000
|
|
680
966
|
BENCHMARK_RUNS=5
|
|
967
|
+
BENCHMARK_FETCH_MIN_BYTES=1
|
|
968
|
+
BENCHMARK_FETCH_WAIT_MS=10
|
|
969
|
+
BENCHMARK_MAX_BYTES=2048
|
|
681
970
|
BENCHMARK_BATCH_SIZE=4096
|
|
682
971
|
BENCHMARK_BATCH_TIMEOUT_MS=2
|
|
683
|
-
|
|
972
|
+
BENCHMARK_KAFKAJS_EACH_MESSAGE_CONCURRENCY=3
|
|
684
973
|
BENCHMARK_MEMORY=1
|
|
974
|
+
BENCHMARK_MEMORY_SAMPLE_MS=100
|
|
975
|
+
BENCHMARK_MEMORY_SETTLE_MS=100
|
|
976
|
+
BENCHMARK_TOPIC=benchmarks
|
|
977
|
+
BENCHMARK_PARTITIONS=3
|
|
685
978
|
```
|
|
686
979
|
|
|
687
|
-
See
|
|
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.
|
|
688
982
|
|
|
689
983
|
## Migration Notes
|
|
690
984
|
|
|
@@ -782,6 +1076,34 @@ const client = new KafkaClient({
|
|
|
782
1076
|
|
|
783
1077
|
Consumer and producer configuration objects also accept their own `configuration` maps.
|
|
784
1078
|
|
|
1079
|
+
### `fetch.max.bytes` Must Be At Least `message.max.bytes`
|
|
1080
|
+
|
|
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:
|
|
1083
|
+
|
|
1084
|
+
```text
|
|
1085
|
+
`fetch.max.bytes` must be >= `message.max.bytes`
|
|
1086
|
+
```
|
|
1087
|
+
|
|
1088
|
+
Keep the fetch caps aligned:
|
|
1089
|
+
|
|
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
|
+
})
|
|
1099
|
+
```
|
|
1100
|
+
|
|
1101
|
+
### Benchmark Numbers Move Between Runs
|
|
1102
|
+
|
|
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.
|
|
1106
|
+
|
|
785
1107
|
## Development
|
|
786
1108
|
|
|
787
1109
|
From the repository root:
|