Confluent Kafka JavaScript Client

repository·master·Indexed 43 days ago

https://github.com/confluentinc/confluent-kafka-javascript

A high-performance Node.js client for Apache Kafka built as a wrapper around librdkafka. Version 1.10.1 provides both a promisified, idiomatic API compatible with kafkajs and a callback-based API compatible with node-rdkafka. It supports pre-built binaries for Linux, macOS, and Windows, and integrates with @confluentinc/schemaregistry for Avro, Protobuf, and JSON Schema support.

Tokens
41.1K
Snippets
106
Records
139
Agent score
52%

What's inside Confluent Kafka JavaScript

  1. Understand the API variants: Promisified vs Callback-based

    master

    The library provides two distinct API styles to accommodate different developer needs:

    1. Promisified API: Recommended for new projects. It provides an idiomatic, Promise-based interface similar to kafkajs.
    2. Callback-based API: Based on node-rdkafka, useful for specific low-level requirements or existing node-rdkafka codebases.

    If you are migrating from kafkajs or node-rdkafka, refer to the specific migration guides in the repository.

  2. Advanced Consumer: Concurrent and Batch Processing

    master

    Concurrent Message Processing

    To process messages concurrently, pass partitionsConsumedConcurrently to the run() method. The library will attempt to dispatch up to that number of eachMessage calls simultaneously. Note that for a single partition, messages are still processed sequentially to guarantee order.

    Batch Processing

    Instead of eachMessage, you can use eachBatch in the run() method. The library creates logical batches (up to a limit of 32).

    If eachBatchAutoResolve is set to false, you must manually resolve offsets using event.resolveOffset(offset) within the callback. This allows you to commit offsets for messages processed even if a subsequent message in the same batch fails.

    consumer.run({
        partitionsConsumedConcurrently: 5,
        eachBatchAutoResolve: false,
        eachBatch: async event => {
            // Process messages...
            // Manually resolve the offset of the last successfully processed message
            event.resolveOffset(event.batch.messages[event.batch.messages.length - 1].message.offset);
        }
    });
  3. Choose between Promisified and Callback-based APIs

    master

    The library provides two distinct API variants depending on your project requirements and migration path:

    1. Promisified API: Recommended for new projects. It uses Promises for asynchronous operations.
    2. Callback-based API: Available for users migrating from existing libraries like kafkajs or node-rdkafka.

    Refer to the specific migration guides in MIGRATION.md if you are moving from kafkajs or node-rdkafka to ensure a smooth transition.

  4. Understand the Kafka message structure

    master

    Messages returned by the KafkaConsumer (via the data event or Stream API) follow this structure:

    {
      "value": "<Buffer>", // message contents as a Buffer
      "size": 2,           // size of the message in bytes
      "topic": "string",  // topic name
      "offset": 1337,      // message offset
      "partition": 1,      // partition number
      "key": "string",    // key (if present)
      "timestamp": 123456  // creation timestamp (if present)
    }
  5. Manage Consumer offsets

    master

    Offsets are automatically committed by default every 5000ms (controlled by auto.commit.interval.ms). Disconnecting or rebalancing also triggers a commit.

    Manual Offset Committing

    To manage offsets manually, set enable.auto.commit: false. When committing, you must provide the next offset to read (current offset + 1).

    const consumer = new Kafka().consumer({
        'bootstrap.servers': '<fill>',
        'group.id': 'test-group',
        'enable.auto.commit': false,
    });
    
    consumer.run({
      eachMessage: async ({ topic, partition, message }) => {
        if (isItTimeToCommit) {
          await consumer.commitOffsets([
            {
              topic,
              partition,
              offset: (parseInt(message.offset, 10) + 1).toString(),
            },
          ]);
        }
      },
    });

    Warning: If using manual commits, you must implement a rebalance_cb to commit offsets during ERR__REVOKE_PARTITIONS, as automatic commits are disabled.

  6. Migrate `run` usage and handle auto-commits

    master

    Properties for auto-committing, such as autoCommit and autoCommitInterval, are no longer passed to the run method. They must be moved to the kafkaJS configuration block in the consumer setup. autoCommitThreshold is no longer supported.

    When autoCommit is enabled, messages are committed periodically based on the autoCommitInterval (default 5 seconds) rather than per-message.

    Key changes in run behavior:

    • heartbeat(): Users no longer need to call heartbeat() manually inside eachMessage or eachBatch callbacks; heartbeats are managed automatically by librdkafka.
    • eachBatch compatibility: An API compatible version of eachBatch is available. The maximum batch size is configured via js.consumer.max.batch.size (defaults to 32).
    • eachBatch properties: eachBatchAutoResolve is supported. Within the callback, offsetLag and offsetLagLow are supported in the returned batch, but uncommittedOffsets is unsupported.
    // Before
    const kafka = new Kafka({ /* ... */ });
    const consumer = kafka.consumer({ /* ... */ });
    await consumer.connect();
    await consumer.subscribe({ topics: ["topic"] });
    consumer.run({
      eachMessage: someFunc,
      autoCommit: true,
      autoCommitInterval: 5000,
    });
    
    // After
    const kafka = new Kafka({ kafkaJS: { /* ... */ } });
    const consumer = kafka.consumer({
      kafkaJS: {
        /* ... */,
        autoCommit: true,
        autoCommitInterval: 5000,
      },
    });
    await consumer.connect();
    await consumer.subscribe({ topics: ["topic"] });
    consumer.run({
      eachMessage: someFunc,
    });
  7. Handle Kafka errors with ErrorCodes

    master

    The library throws KafkaJSErrors. To handle them correctly, check the code property against the exported ErrorCodes object.

    • Retriable/Abortable: For transactional producers, errors include retriable and abortable properties.
    • Fatal Errors: In rare cases (e.g., with idempotent/transactional producers), an error may have a fatal attribute. If fatal is true, the client is unusable and you should terminate it and create a new one.
    • Resilience: Once successfully connected, the client stays usable despite most errors and will attempt to reconnect automatically.
    const { Kafka, ErrorCodes, isKafkaJSError } = require('@confluentinc/kafka-javascript').KafkaJS;
    
    try {
      await producer.send({ /*...*/ });
    } catch (e) {
      if (!isKafkaJSError(e)) throw e;
      if (e.code === ErrorCodes.ERR__MSG_TIMED_OUT) {
        // Implement backoff and retry
      }
    }
  8. Optimize Producer throughput and latency

    master

    The library batches messages internally per partition. You can control this behavior using configuration:

    High Throughput

    To maximize throughput, increase linger.ms to allow more messages to accumulate in a batch. Crucially, do not await individual send() calls; instead, collect the promises and await them all at once to allow batching to work effectively.

    const reportPromises = [];
    for (const topicMessages of topicsMessages) {
      reportPromises.push(
        producer.send({topic: topicMessages.topic, messages: topicMessages.messages}));
    }
    
    await reportPromises;

    Low Latency

    To minimize latency, set linger.ms to 0 or 1 and await each send() call individually.

    Manual Flushing

    If you have a large linger.ms but need to send messages immediately, use flush() to clear the internal buffer.

    // Unawaited send.
    const reportPromise = producer.send(/*...*/);
    
    // Try to send everything, waiting up to 1s.
    await producer.flush({ timeout: 1000 });
  9. Configure Logging

    master

    The library has a built-in logger (defaults to console) and supports custom loggers.

    Using the Built-in Logger

    const { logLevel } = require('@confluentinc/kafka-javascript').KafkaJS;
    const libraryLogger = producer.logger();
    libraryLogger.info('Message');
    libraryLogger.setLogLevel(logLevel.INFO);

    Debugging Library Internals

    To see internal librdkafka logs, set the debug property in your configuration to a comma-separated list of components or all. Note: This must be set before connecting, as it cannot be changed on the fly.

    Custom Logger

    Provide a custom logger via the kafkaJS.logger key in the configuration. The logger must implement setLogLevel(logLevel), info(message, extra), error(message, extra), warn(message, extra), and debug(message, extra).

    class MyLogger {
      constructor() {}
      setLogLevel(logLevel) {}
      info(message, extra) {}
      error(message, extra) {}
      warn(message, extra) {}
      debug(message, extra) {}
    }
    
    const producer = new Kafka().producer({
        'bootstrap.servers': '<fill>',
        'debug': 'all',
        'kafkaJS': {
            'logger': new MyLogger(),
        }
    });
  10. Use Transactional Producers for Exactly-Once Semantics (EOS)

    master

    To ensure messages are produced exactly once, use a transactional producer by providing a unique transactional.id in the configuration.

    Note: A transactional producer cannot be used as a non-transactional producer; every message must be wrapped in a transaction.

    const producer = new Kafka().producer({
        'bootstrap.servers': '<fill>',
        'transactional.id': 'my-transactional-id', // Must be unique per instance
    });
    
    await producer.connect();
    await producer.transaction();
    await producer.send({topic: 'topic', messages: [{value: 'message'}]});
    await producer.commit();

    EOS Pattern: Consume-Process-Produce

    You can achieve Exactly-Once Semantics when consuming from one topic and producing to another by including the consumer offsets in the producer transaction.

    consumer.run({
        eachMessage: async ({ topic, partition, message }) => {
            try {
                const transaction = await producer.transaction();
                await transaction.send({
                    topic: 'produceTopic',
                    messages: [{ value: 'consumed: ' + message.value.toString() }]
                });
    
                await transaction.sendOffsets({
                    consumer,
                    topics: [{
                        topic,
                        partitions: [{ partition, offset: String(Number(message.offset) + 1) }]
                    }]
                });
    
                await transaction.commit();
            } catch (e) {
                await transaction.abort();
            }
        },
    });