# Kafka

A FlowFuse-certified package that lets you connect to Apache Kafka, send and receive messages, run consumer groups, commit or roll back messages by hand, manage topics, and read offsets from your flows.

## Get the Certified Node in FlowFuse

The Kafka package is part of the FlowFuse Hub Certified Nodes catalogue, which is part of the **FlowFuse Hub** offering. Please contact our sales team at [Contact us](/contact-us/) to learn more or to request access.

### Installation steps

1. Open your instance in the FlowFuse editor.
2. Click the menu icon (☰) in the top-right corner.
3. Select **Manage palette**.
4. Go to the **Install** tab.
5. Switch to the **FlowFuse Hub Certified Nodes** category.
6. Search for `@flowfuse-certified-nodes/kafka`.
7. Click **Install**.

The Kafka nodes then appear in your palette, ready to drag onto the canvas.

> **Note:** Newly installed nodes are picked up automatically, no restart needed. Restart is only required when you update a node that's already installed: restart any remote instance or hosted instance running the previous version.

## Features

- Send messages to any topic, and set the key, partition, and compression
- Read from several topics at once, each with its own starting offset and partition
- Match topics by pattern, so new topics are picked up without a redeploy
- Consumer groups, so several consumers can share the work
- Commit and roll back by hand, instead of letting Kafka mark messages as done straight away
- Convert `/` to `.` in topic names, so MQTT-style topics work with Kafka
- Manage topics from a flow: create, delete, and add partitions
- Read offsets, so you can measure how far behind a consumer is
- TLS, including self-signed certificates, and SASL PLAIN username and password authentication

## Use case

Kafka stores messages instead of just passing them on. MQTT sends a message and forgets it. Kafka keeps it for a set period, so a system that was offline can catch up later, and a new consumer can read messages that arrived before it started.

That makes it a good fit for these jobs:

- **Send production data to IT systems.** Write each reading to a topic with **Kafka Producer**. An ERP, MES, or data lake reads that topic in its own time. It never connects to your Node-RED instance directly.
- **Feed several systems from one topic.** Point more than one **Kafka Consumer** at the same topic. Each one gets every message, and each one keeps its own place in the topic. A dashboard, an alert flow, and a historian can all read the same data without getting in each other's way.
- **Ride out an outage.** If a system downstream goes down, messages build up in the topic instead of being lost. You can also start a consumer at an older offset to read a shift's data again after fixing a bug.
- **Handle more messages.** Use **Kafka Consumer Group** when one consumer cannot keep up. Members of a group split the work rather than each doing all of it.
- **Make sure nothing is dropped.** Turn off auto commit and use **Kafka Commit** and **Kafka Rollback**. A message is only marked as done once your flow has finished with it, so a failure part-way through does not lose data.

## Nodes in this package

<table>
<thead>
  <tr>
    <th>
      Node
    </th>
    
    <th>
      Inputs
    </th>
    
    <th>
      Outputs
    </th>
    
    <th>
      Purpose
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <strong>
        Kafka Broker
      </strong>
    </td>
    
    <td>
      –
    </td>
    
    <td>
      –
    </td>
    
    <td>
      Config node holding the hosts, timeouts, and security details
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Kafka Producer
      </strong>
    </td>
    
    <td>
      1
    </td>
    
    <td>
      0
    </td>
    
    <td>
      Sends a message to a topic
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Kafka Consumer
      </strong>
    </td>
    
    <td>
      0
    </td>
    
    <td>
      1
    </td>
    
    <td>
      Reads messages from one or more topics
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Kafka Consumer Group
      </strong>
    </td>
    
    <td>
      0
    </td>
    
    <td>
      1
    </td>
    
    <td>
      Reads messages as part of a group, sharing topics with other members
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Kafka Commit
      </strong>
    </td>
    
    <td>
      1
    </td>
    
    <td>
      2
    </td>
    
    <td>
      Marks a message as done, when auto commit is off
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Kafka Rollback
      </strong>
    </td>
    
    <td>
      1
    </td>
    
    <td>
      2
    </td>
    
    <td>
      Leaves a message uncommitted and closes the consumer
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Kafka Admin
      </strong>
    </td>
    
    <td>
      1
    </td>
    
    <td>
      2
    </td>
    
    <td>
      Runs admin commands, such as creating topics or listing groups
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Kafka Offset
      </strong>
    </td>
    
    <td>
      1
    </td>
    
    <td>
      2
    </td>
    
    <td>
      Reads offset details for a topic or a consumer group
    </td>
  </tr>
</tbody>
</table>

Where a node has two outputs, the first fires on success and the second on failure. Wire the second one up so errors don't disappear.

## Set up the connection (Kafka Broker)

Create **one Kafka Broker config node for each Kafka cluster**, then point your other nodes at it. You only enter the connection details once, and the nodes that use it share a single connection to the cluster.

Five nodes have a **Broker** field: Kafka Producer, Kafka Consumer, Kafka Consumer Group, Kafka Admin, and Kafka Offset. Kafka Commit and Kafka Rollback do not, because they work out which connection to use from the message they receive.

1. Drag one of those five nodes onto the canvas and open it
2. Click the **+** button next to **Broker**
3. Give the connection a **Name**, such as `Production Kafka`
4. Fill in the **Connection** tab, then the **Options** tab if you need to
5. Click **Add**, then **Done**

Once the connection exists, the other nodes pick it from their own **Broker** dropdown. Use the pencil button beside that dropdown to edit it later.

### Connection tab

<table>
<thead>
  <tr>
    <th>
      Field
    </th>
    
    <th>
      What it does
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <strong>
        Hosts
      </strong>
    </td>
    
    <td>
      A table of brokers to start from. Click <strong>
        +Add
      </strong>
      
       for each one and fill in its <strong>
        Name
      </strong>
      
       (hostname or IP) and <strong>
        Port
      </strong>
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Hosts process.env name
      </strong>
    </td>
    
    <td>
      The name of an environment variable holding the host list, used instead of the table. See below
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        via Zookeeper
      </strong>
    </td>
    
    <td>
      <code>
        true
      </code>
      
      /<code>
        false
      </code>
      
      , default <code>
        true
      </code>
      
      . Legacy field from when clients connected through ZooKeeper. Leave it as it is
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Use TLS
      </strong>
    </td>
    
    <td>
      Tick this if your cluster requires TLS. Ticking it also reveals a field for picking a TLS configuration node, where the certificates go
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Self Sign
      </strong>
    </td>
    
    <td>
      Controls the certificate check behind <strong>
        Use TLS
      </strong>
      
      . Left unticked, certificates are not verified, which is what lets a self-signed certificate through. Tick it to have the certificate verified. Turning verification off is for test environments, and best avoided in production
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Use Credentials (SASL plain)
      </strong>
    </td>
    
    <td>
      Tick this to authenticate with a username and password
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        User
      </strong>
    </td>
    
    <td>
      Username, used when <strong>
        Use Credentials
      </strong>
      
       is ticked
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Password
      </strong>
    </td>
    
    <td>
      Password, used when <strong>
        Use Credentials
      </strong>
      
       is ticked
    </td>
  </tr>
</tbody>
</table>

Authentication here is SASL PLAIN only, so the username and password travel in the clear unless you also turn on **Use TLS**. Tick both together on anything but a local test cluster.

### Keeping hosts out of your flows

Typing hostnames straight into the table works, but it ties the flow to one cluster. If the same flow runs against test and production, keep the hosts in an instance environment variable instead.

The **Hosts process.env name** field takes the *name* of the variable, not its value:

1. Go to **Instance Settings → Environment** and add a variable, for example `KAFKA_HOSTS`. Its value is the host list as JSON:```text
[{"host":"kafka-1.example.com","port":9092}]
```
2. Restart the instance so Node-RED picks up the variable.
3. In the Kafka Broker config node, put `KAFKA_HOSTS` in the **Hosts process.env name** field.

Each environment can now point at its own cluster, and the flow stays the same. See [Using environment variables in Node-RED](/blog/2023/01/environment-variables-in-node-red/) for more on environment variables.

### Options tab

These control the connection itself rather than anything about your messages. The defaults are sensible, so most people never touch this tab.

<table>
<thead>
  <tr>
    <th>
      Field
    </th>
    
    <th>
      Options
    </th>
    
    <th>
      Default
    </th>
    
    <th>
      What it does
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <strong>
        Connect Timeout (ms)
      </strong>
    </td>
    
    <td>
      
    </td>
    
    <td>
      10000
    </td>
    
    <td>
      How long to wait when opening a connection before giving up
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Request Timeout (ms)
      </strong>
    </td>
    
    <td>
      
    </td>
    
    <td>
      30000
    </td>
    
    <td>
      How long to wait for a reply from a broker
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Auto Connect
      </strong>
    </td>
    
    <td>
      <code>
        True
      </code>
      
      /<code>
        False
      </code>
    </td>
    
    <td>
      True
    </td>
    
    <td>
      <code>
        True
      </code>
      
       connects on deploy. <code>
        False
      </code>
      
       waits until a node actually needs the connection
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Idle Connection (mins)
      </strong>
    </td>
    
    <td>
      
    </td>
    
    <td>
      5
    </td>
    
    <td>
      How long a quiet connection is kept open before it is dropped
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Reconnect On Idle
      </strong>
    </td>
    
    <td>
      <code>
        True
      </code>
      
      /<code>
        False
      </code>
    </td>
    
    <td>
      True
    </td>
    
    <td>
      <code>
        True
      </code>
      
       reopens the connection after it has been dropped, so a quiet flow keeps working. Set to <code>
        False
      </code>
      
       only if you want the connection to stay closed
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Max Async Requests (ms)
      </strong>
    </td>
    
    <td>
      
    </td>
    
    <td>
      10
    </td>
    
    <td>
      How many requests the client keeps in flight at once. The label says ms, but it is a count, not a time
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Check Interval (secs)
      </strong>
    </td>
    
    <td>
      
    </td>
    
    <td>
      10
    </td>
    
    <td>
      How often the brokers are checked to see whether they can still be reached
    </td>
  </tr>
</tbody>
</table>

On a slow or busy network, raising **Connect Timeout** and **Request Timeout** is usually the first thing to try.

The examples on this page use `127.0.0.1` and `9092` as stand-ins for a local Kafka broker. Replace them with your own details.

## Send messages (Kafka Producer)

**Kafka Producer** sends `msg.payload` to a topic. It has one input and no output.

<table>
<thead>
  <tr>
    <th>
      Field
    </th>
    
    <th>
      Options
    </th>
    
    <th>
      Default
    </th>
    
    <th>
      What it does
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <strong>
        Type
      </strong>
    </td>
    
    <td>
      <code>
        Producer
      </code>
      
      , <code>
        High Level (round robin)
      </code>
    </td>
    
    <td>
      Producer
    </td>
    
    <td>
      <code>
        Producer
      </code>
      
       writes to the partition you pick. <code>
        High Level (round robin)
      </code>
      
       spreads messages across the topic's partitions using round robin instead
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Broker
      </strong>
    </td>
    
    <td>
      your Kafka Broker configs
    </td>
    
    <td>
      
    </td>
    
    <td>
      Which connection to send through
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Topic
      </strong>
    </td>
    
    <td>
      
    </td>
    
    <td>
      
    </td>
    
    <td>
      The topic to write to. Can also come from <code>
        msg.topic
      </code>
      
      , while <strong>
        Message Topic Overides
      </strong>
      
       is on
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Message Topic Overides
      </strong>
    </td>
    
    <td>
      <code>
        true
      </code>
      
      /<code>
        false
      </code>
    </td>
    
    <td>
      true
    </td>
    
    <td>
      When <code>
        true
      </code>
      
      , <code>
        msg.topic
      </code>
      
       takes the place of the <strong>
        Topic
      </strong>
      
       set here. Set it to <code>
        false
      </code>
      
       to pin the node to one topic
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Message Overides
      </strong>
    </td>
    
    <td>
      <code>
        true
      </code>
      
      /<code>
        false
      </code>
    </td>
    
    <td>
      true
    </td>
    
    <td>
      When <code>
        true
      </code>
      
      , <code>
        msg.key
      </code>
      
      , <code>
        msg.partition
      </code>
      
      , and <code>
        msg.attributes
      </code>
      
       take the place of the values set here
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Require Ack
      </strong>
    </td>
    
    <td>
      <code>
        0
      </code>
      
      , <code>
        1
      </code>
    </td>
    
    <td>
      1
    </td>
    
    <td>
      How many brokers must confirm the write. The node treats <code>
        0
      </code>
      
       the same as <code>
        1
      </code>
      
      , so a write always waits for the partition leader
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Ack Timeout (Ms)
      </strong>
    </td>
    
    <td>
      
    </td>
    
    <td>
      100
    </td>
    
    <td>
      How long to wait for those confirmations
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Partitioner
      </strong>
    </td>
    
    <td>
      see <a href="#partitioner-options">
        Partitioner options
      </a>
    </td>
    
    <td>
      default
    </td>
    
    <td>
      How a partition is picked when you haven't set one
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Key
      </strong>
    </td>
    
    <td>
      
    </td>
    
    <td>
      
    </td>
    
    <td>
      The message key. Consumers read it back from <code>
        msg._kafka.key
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Partition
      </strong>
    </td>
    
    <td>
      
    </td>
    
    <td>
      0
    </td>
    
    <td>
      The partition to write to. Numbering starts at <code>
        0
      </code>
      
      . Ignored when <strong>
        Type
      </strong>
      
       is <code>
        High Level (round robin)
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Compression
      </strong>
    </td>
    
    <td>
      see <a href="#compression-options">
        Compression options
      </a>
    </td>
    
    <td>
      none
    </td>
    
    <td>
      Compresses messages before sending. Worth turning on for high volumes of text
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Convert message from JSON
      </strong>
    </td>
    
    <td>
      checkbox
    </td>
    
    <td>
      Off
    </td>
    
    <td>
      Turns an object payload into text for you, so you don't need a separate <code>
        json
      </code>
      
       node
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Topic replace / with .
      </strong>
    </td>
    
    <td>
      checkbox
    </td>
    
    <td>
      Off
    </td>
    
    <td>
      Converts the first <code>
        /
      </code>
      
       in a topic name coming from <code>
        msg.topic
      </code>
      
       to <code>
        .
      </code>
      
      , so MQTT-style topics work with Kafka
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        producer max Q depth
      </strong>
    </td>
    
    <td>
      
    </td>
    
    <td>
      1000
    </td>
    
    <td>
      How many messages the node holds while waiting to send
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Deadletter Topic
      </strong>
    </td>
    
    <td>
      
    </td>
    
    <td>
      deadletter
    </td>
    
    <td>
      Where a message goes when the send fails. The whole <code>
        msg
      </code>
      
       is written there as gzipped JSON. Clear the field to discard failed messages instead
    </td>
  </tr>
</tbody>
</table>

**Key** is a typed input, so you can enter a fixed string or point it at a message property.

**Require Ack** offers only `0` and `1`, and the node falls back to `1` when it is left at `0`, so every write waits for the partition leader to confirm it. Waiting for every replica, which Kafka itself writes as `acks=-1`, is not available from this node.

### Partitioner options

The partitioner decides which partition a message goes to. It only comes into play when the message itself doesn't say: set **Partition** on the node, or `msg.partition` on the message, and the partitioner is bypassed.

<table>
<thead>
  <tr>
    <th>
      Option
    </th>
    
    <th>
      What it does
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        default
      </code>
    </td>
    
    <td>
      Sends every message to the same partition, the first one. Fine for a single-partition topic, and a common surprise on a topic with several
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        random
      </code>
    </td>
    
    <td>
      Picks a partition at random for each message. Spreads the load, but consecutive messages can land out of order relative to each other
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        cyclic
      </code>
    </td>
    
    <td>
      Works through the partitions in turn, one message each. An even spread, and more predictable than random
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        keyed
      </code>
    </td>
    
    <td>
      Hashes the <strong>
        Key
      </strong>
      
       to choose the partition, so the same key always lands on the same partition. Messages for one key then stay in the order you sent them
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        custom
      </code>
    </td>
    
    <td>
      Uses a partitioner supplied in code instead of one of the strategies above
    </td>
  </tr>
</tbody>
</table>

Pick `keyed` when order matters within a group of messages — readings from one machine, events for one order number — and set **Key** to whatever identifies that group. Kafka only guarantees order inside a partition, so this is the setting that keeps a machine's readings sequential.

Pick `cyclic` when you only care about spreading work evenly and order doesn't matter.

`default` is worth a second look if you have a multi-partition topic and find everything piling into partition `0`.

### Compression options

The **Compression** dropdown offers:

<table>
<thead>
  <tr>
    <th>
      Option
    </th>
    
    <th>
      Notes
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        none
      </code>
    </td>
    
    <td>
      The default. No compression
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        gzip
      </code>
      
      , <code>
        gzip best speed
      </code>
      
      , <code>
        gzip best compression
      </code>
    </td>
    
    <td>
      Widely understood by other Kafka clients
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        zip
      </code>
      
      , <code>
        zip best speed
      </code>
      
      , <code>
        zip best compression
      </code>
    </td>
    
    <td>
      
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        Lempel-Ziv-Markov best speed
      </code>
      
      , <code>
        Lempel-Ziv-Markov best compression
      </code>
    </td>
    
    <td>
      LZMA
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        Brotli
      </code>
    </td>
    
    <td>
      
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        Snappy
      </code>
    </td>
    
    <td>
      Fast, with modest size savings. A common choice for Kafka
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        Deflate
      </code>
    </td>
    
    <td>
      
    </td>
  </tr>
</tbody>
</table>

Where a codec has **best speed** and **best compression** variants, you are trading CPU time against message size. The plain option sits between the two.

Whatever you pick, whatever reads the topic has to understand it. If your consumers are also Kafka Consumer nodes, set the same codec on their **Decompression** field. If anything else reads the topic, stick to `gzip` or `Snappy`, which every Kafka client handles. The other codecs here are not part of the standard Kafka set, so a Java or Python consumer may not be able to read them.

## Receive messages (Kafka Consumer)

**Kafka Consumer** has no input. It sends out one `msg` for every Kafka message that arrives.

One field sits above the tabs, alongside **Name**:

<table>
<thead>
  <tr>
    <th>
      Field
    </th>
    
    <th>
      Options
    </th>
    
    <th>
      What it does
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <strong>
        Broker
      </strong>
    </td>
    
    <td>
      your Kafka Broker configs
    </td>
    
    <td>
      Which connection to read through
    </td>
  </tr>
</tbody>
</table>

The remaining settings are split across four tabs: **Topics**, **Options**, **Fetch**, and **Encoding**.

### Topics tab

Add a row to the **Topic** table for each topic you want, using **+Add**. Each row has three columns:

<table>
<thead>
  <tr>
    <th>
      Column
    </th>
    
    <th>
      What it does
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <strong>
        Name
      </strong>
    </td>
    
    <td>
      The topic name
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Offset
      </strong>
    </td>
    
    <td>
      Where to start reading from
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Partition
      </strong>
    </td>
    
    <td>
      Which partition to read
    </td>
  </tr>
</tbody>
</table>

Tick **wildcard (regex)** to treat the names as patterns instead of exact topics. Matching topics are picked up as they appear and dropped when they go away. Kafka is checked once a minute, so a new topic can take up to a minute to show up.

This is the setting to reach for when a topic is created per machine or per line. A pattern like `line.*\.temperature` keeps working as lines are added, with no redeploy.

### Options tab

<table>
<thead>
  <tr>
    <th>
      Field
    </th>
    
    <th>
      Options
    </th>
    
    <th>
      Default
    </th>
    
    <th>
      What it does
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <strong>
        Group Id
      </strong>
    </td>
    
    <td>
      
    </td>
    
    <td>
      <code>
        kafka-node-group
      </code>
    </td>
    
    <td>
      The group this consumer belongs to. Change it if two flows read the same topic and shouldn't share a position
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Auto Commit
      </strong>
    </td>
    
    <td>
      <code>
        True
      </code>
      
      /<code>
        False
      </code>
    </td>
    
    <td>
      True
    </td>
    
    <td>
      <code>
        True
      </code>
      
       lets Kafka mark messages as done on a timer. <code>
        False
      </code>
      
       hands that job to Kafka Commit and Kafka Rollback
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Auto Commit Interval (Ms)
      </strong>
    </td>
    
    <td>
      
    </td>
    
    <td>
      5000
    </td>
    
    <td>
      How often those automatic commits happen. Ignored when <strong>
        Auto Commit
      </strong>
      
       is <code>
        False
      </code>
    </td>
  </tr>
</tbody>
</table>

### Fetch tab

How much data the consumer pulls at a time. Leave these alone unless you are chasing throughput or latency.

<table>
<thead>
  <tr>
    <th>
      Field
    </th>
    
    <th>
      Default
    </th>
    
    <th>
      What it does
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <strong>
        Fetch Max Wait (Ms)
      </strong>
    </td>
    
    <td>
      100
    </td>
    
    <td>
      How long the broker holds a reply back, hoping more data turns up
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Fetch Min Bytes
      </strong>
    </td>
    
    <td>
      1
    </td>
    
    <td>
      The smallest amount of data worth replying with
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Fetch Max Bytes
      </strong>
    </td>
    
    <td>
      1048576
    </td>
    
    <td>
      The most data to pull in one go, 1 MB by default
    </td>
  </tr>
</tbody>
</table>

Raising **Fetch Min Bytes** and **Fetch Max Wait** means fewer, fuller round trips, which suits high message rates. Lowering them gets messages to your flow sooner, which suits alerting.

### Encoding tab

<table>
<thead>
  <tr>
    <th>
      Field
    </th>
    
    <th>
      Options
    </th>
    
    <th>
      Default
    </th>
    
    <th>
      What it does
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <strong>
        From Offset
      </strong>
    </td>
    
    <td>
      <code>
        latest
      </code>
      
      , <code>
        earliest
      </code>
      
      , <code>
        none
      </code>
    </td>
    
    <td>
      latest
    </td>
    
    <td>
      Where to start when the <strong>
        Topic
      </strong>
      
       table has no offset. <code>
        latest
      </code>
      
       reads only new messages, <code>
        earliest
      </code>
      
       starts from the oldest message still held, <code>
        none
      </code>
      
       fails if there is no committed position
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Encoding
      </strong>
    </td>
    
    <td>
      <code>
        utf8
      </code>
      
      , <code>
        raw
      </code>
    </td>
    
    <td>
      utf8
    </td>
    
    <td>
      How the message value is decoded. <code>
        raw
      </code>
      
       gives you the bytes untouched, for binary payloads
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Key Encoding
      </strong>
    </td>
    
    <td>
      <code>
        utf8
      </code>
      
      , <code>
        raw
      </code>
    </td>
    
    <td>
      utf8
    </td>
    
    <td>
      The same, for the message key
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Convert message to JSON
      </strong>
    </td>
    
    <td>
      checkbox
    </td>
    
    <td>
      Off
    </td>
    
    <td>
      Parses the message into an object for you, so you don't need a separate <code>
        json
      </code>
      
       node
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Decompression
      </strong>
    </td>
    
    <td>
      <code>
        none
      </code>
      
      , <code>
        gzip
      </code>
      
      , <code>
        zip
      </code>
      
      , <code>
        Lempel-Ziv-Markov
      </code>
      
      , <code>
        Brotli
      </code>
      
      , <code>
        Snappy
      </code>
      
      , <code>
        Inflate
      </code>
    </td>
    
    <td>
      none
    </td>
    
    <td>
      How to decompress messages that were compressed by the producer. Must match the codec the producer used
    </td>
  </tr>
</tbody>
</table>

The **Decompression** list is shorter than the producer's, because the *best speed* and *best compression* variants only affect compressing. Pick the plain codec: a producer set to `gzip best compression` is read back with `gzip`. The producer's `Deflate` is listed here as `Inflate`, which is the same codec.

Pick `earliest` on **From Offset** when a new consumer needs the history that is already in the topic. Pick `latest` when it should only see what happens from now on.

### What arrives in the message

Every message the node sends out carries three things:

- **msg.payload** — the message itself. With the default `utf8` encoding this is text, unless you turn on **Convert message to JSON**. See [Payloads are text](#payloads-are-text)
- **msg.topic** — the Kafka topic the message came from, so a flow reading several topics can tell them apart
- **msg._kafka** — the details from Kafka: `topic`, `partition`, `offset`, `key` (the key set by the producer), and `highWaterOffset` (the end of that partition, so `highWaterOffset - offset` is how far behind this message was read)

Keep `msg._kafka` on the message if you plan to commit or roll back later, because Kafka Commit and Kafka Rollback both need it.

## Share the work (Kafka Consumer Group)

**Kafka Consumer Group** reads messages the same way as Kafka Consumer, but members that use the same **Group Id** split the work between them. Kafka gives each topic and partition to one member only, and hands them out again when a member joins or leaves.

Use a group when you need to handle more messages. Use separate Kafka Consumer nodes when every consumer needs to see every message.

Above the tabs it has **Type** and **Broker**. **Type** offers only `Base`, so there is nothing to decide there. Kafka Consumer has no **Type** field at all.

### Topics tab

Add a row for each topic. Unlike Kafka Consumer, you only give the **Name** here — there is no per-topic offset or partition.

### Options tab

<table>
<thead>
  <tr>
    <th>
      Field
    </th>
    
    <th>
      Options
    </th>
    
    <th>
      Default
    </th>
    
    <th>
      What it does
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <strong>
        Group Id
      </strong>
    </td>
    
    <td>
      
    </td>
    
    <td>
      <code>
        aGroup
      </code>
    </td>
    
    <td>
      The group name. Every member sharing the work uses the same one
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Session Timeout (ms)
      </strong>
    </td>
    
    <td>
      
    </td>
    
    <td>
      15000
    </td>
    
    <td>
      How long before a quiet member is treated as gone and its work reassigned
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Protocol
      </strong>
    </td>
    
    <td>
      <code>
        Round Robin
      </code>
      
      , <code>
        Range
      </code>
    </td>
    
    <td>
      Round Robin
    </td>
    
    <td>
      How partitions are shared out. Round robin deals them out one at a time, range gives each member a contiguous block
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Encoding
      </strong>
    </td>
    
    <td>
      <code>
        utf8
      </code>
      
      , <code>
        raw
      </code>
    </td>
    
    <td>
      utf8
    </td>
    
    <td>
      How the message value is decoded
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        From Offset
      </strong>
    </td>
    
    <td>
      <code>
        latest
      </code>
      
      , <code>
        earliest
      </code>
      
      , <code>
        none
      </code>
    </td>
    
    <td>
      latest
    </td>
    
    <td>
      Where a brand new group starts reading. Once the group has committed a position, that position wins
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Commit Offsets On First Join
      </strong>
    </td>
    
    <td>
      <code>
        True
      </code>
      
      /<code>
        False
      </code>
    </td>
    
    <td>
      True
    </td>
    
    <td>
      Records a starting position as soon as the group forms, rather than waiting for the first commit
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Out Of Range Offset
      </strong>
    </td>
    
    <td>
      <code>
        earliest
      </code>
      
      , <code>
        latest
      </code>
      
      , <code>
        none
      </code>
    </td>
    
    <td>
      earliest
    </td>
    
    <td>
      Where to go if the stored offset no longer exists, usually because those messages have aged out
    </td>
  </tr>
</tbody>
</table>

A group cannot split the work more ways than there are topics and partitions. Any extra members sit idle, which is worth remembering before adding a fourth consumer to a three-partition topic.

There is no **Auto Commit** setting here, and no **Convert message to JSON**. Use a `json` node in the flow if you need to parse the payload.

## Commit and roll back (Kafka Commit, Kafka Rollback)

By default a consumer commits automatically, so Kafka treats a message as done as soon as it hands it over. If your flow then fails, the message is gone. Committing by hand instead means a message is only marked as done once your flow has finished with it.

### What each node does

**Kafka Commit** commits a message that was read without auto commit. Put it at the end of the path that worked.

**Kafka Rollback** rolls the message back and closes the consumer that read it. The message stays uncommitted and Kafka stops handing it out. Put it on the error path, then fix the problem and open the consumer again to carry on.

### What they need to work

- **Auto commit must be off.** Set **Auto Commit** to `False` on the Kafka Consumer node. Neither node does anything useful while the consumer is committing on its own.
- **The message must still carry msg._kafka.** Both nodes use it to find the message and its consumer. A `change` or `function` node that rebuilds `msg` from scratch will remove it.
- **They only work with Kafka Consumer.** Kafka Consumer Group has no auto commit setting to turn off.

Neither node has a **Broker** field, or any setting beyond **Name** — they get the connection from the message. Both have two outputs: the first for success, the second for failure.

> **Note**: Kafka keeps sending messages to a consumer whether or not an earlier commit is still outstanding. A commit therefore marks every message up to that point as done, not just the one that triggered it. If that matters to you, keep the number of messages in flight low.

## Administration (Kafka Admin)

**Kafka Admin** runs one admin command against the cluster. These are the same jobs you would otherwise do with the `kafka-topics.sh` and `kafka-configs.sh` scripts on a broker. Running them from a flow means you can create topics as part of a deployment, show cluster state on a dashboard, or check consumer groups on a schedule.

The node has only **Name** and **Broker**. Everything else comes from the message. It has two outputs, one for success and one for failure.

### How to send a command

Every command uses the same two message properties:

- **msg.topic** — the command name, spelled as in the tables below. Capital letters matter
- **msg.payload** — the command's arguments, as JSON. Leave it out for commands that take none

The reply comes back on `msg.payload` from the first output.

Watch out for one thing: `msg.topic` holds the **command name**, not a Kafka topic name. The topic a command acts on goes in `msg.payload`. This trips up most people the first time.

An `inject` node is the easiest way to drive it. Set its **Topic** to the command and its **Payload** to the JSON, wire it into Kafka Admin, and add a `debug` node to each output so you see both the reply and any error.

### Commands that take no arguments

Send these with `msg.topic` set and no payload.

<table>
<thead>
  <tr>
    <th>
      Command
    </th>
    
    <th>
      What comes back
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        listTopics
      </code>
    </td>
    
    <td>
      Every topic in the cluster, with its partitions and replicas
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        listGroups
      </code>
    </td>
    
    <td>
      All groups the coordinator knows about
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        listConsumerGroups
      </code>
    </td>
    
    <td>
      Consumer groups only
    </td>
  </tr>
</tbody>
</table>

These three are the only commands that run without a payload. Everything else in the next table needs one, and a command name the node doesn't recognise comes out of the second output as an `invalid request` error.

### Commands that take arguments

Arguments always go in `msg.payload`.

<table>
<thead>
  <tr>
    <th>
      Job
    </th>
    
    <th>
      Commands
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <strong>
        Topics
      </strong>
    </td>
    
    <td>
      <code>
        createTopics
      </code>
      
      , <code>
        deleteTopics
      </code>
      
      , <code>
        describeTopics
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Partitions
      </strong>
    </td>
    
    <td>
      <code>
        createPartitions
      </code>
      
      , <code>
        electPreferredLeaders
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Settings
      </strong>
    </td>
    
    <td>
      <code>
        alterConfigs
      </code>
      
      , <code>
        incrementalAlterConfigs
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Consumer groups
      </strong>
    </td>
    
    <td>
      <code>
        describeGroups
      </code>
      
      , <code>
        describeConsumerGroups
      </code>
      
      , <code>
        deleteConsumerGroups
      </code>
      
      , <code>
        listConsumerGroupOffsets
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Records
      </strong>
    </td>
    
    <td>
      <code>
        deleteRecords
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        ACLs
      </strong>
    </td>
    
    <td>
      <code>
        createAcls
      </code>
      
      , <code>
        describeAcls
      </code>
      
      , <code>
        deleteAcls
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Log directories
      </strong>
    </td>
    
    <td>
      <code>
        describeLogDirs
      </code>
      
      , <code>
        alterReplicaLogDirs
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Configuration
      </strong>
    </td>
    
    <td>
      <code>
        describeConfigs
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Delegation tokens
      </strong>
    </td>
    
    <td>
      <code>
        createDelegationToken
      </code>
      
      , <code>
        renewDelegationToken
      </code>
      
      , <code>
        expireDelegationToken
      </code>
    </td>
  </tr>
</tbody>
</table>

### Examples

**Create a topic with three partitions.** Set `msg.topic` to `createTopics` and `msg.payload` to:

```json
[{ "topic": "Temperature", "partitions": 3, "replicationFactor": 1 }]
```

The payload is an array, so one message can create several topics at once. Add `configEntries` to set topic settings at the same time, such as keeping messages for seven days:

```json
[{
  "topic": "Temperature",
  "partitions": 3,
  "replicationFactor": 1,
  "configEntries": [{ "name": "retention.ms", "value": "604800000" }]
}]
```

**List every topic.** Set `msg.topic` to `listTopics` and send no payload.

**Read a topic's settings.** Set `msg.topic` to `describeConfigs` and `msg.payload` to `{ "type": "topic", "name": "Temperature" }`. Unlike the other commands, this one takes a plain object rather than an array, and `type` can be `topic` or `broker`.

**Add partitions to a topic.** Set `msg.topic` to `createPartitions`. You can only ever add partitions, never remove them, so this cannot be undone. Adding partitions also changes which partition each key goes to, so existing keys stop landing where they used to.

**Delete a topic.** Set `msg.topic` to `deleteTopics` and put the topic names in the payload. This only works if the brokers have `delete.topic.enable` turned on, which is the default from Kafka 1.0 onwards. If it is off, the request is accepted and then ignored.

### Things to watch out for

Creating a topic takes a moment. The reply means the request was accepted, not that every broker knows about the topic yet, so a `listTopics` sent straight afterwards may not include it. If a flow creates a topic and then writes to it, add a short delay rather than wiring the two together directly.

`replicationFactor` cannot be higher than your number of brokers. Asking for `3` on a single-broker test cluster will fail, so use `1` locally and set the real value per environment with an environment variable.

You may not need to create topics at all. If the brokers have `auto.create.topics.enable` turned on, writing to an unknown topic creates it with the default settings. That is handy while testing and usually unwanted in production, because a typo quietly creates a new topic instead of failing.

Admin commands also need the right permissions. On a cluster with ACLs, the credentials in your Kafka Broker config need cluster-level rights, so read-only credentials that work fine for a consumer will not work here.

## Offsets (Kafka Offset)

**Kafka Offset** reads offset details from Kafka. Like Kafka Admin, the node has only **Name** and **Broker**, takes everything else from the message, and has two outputs.

It uses two message properties:

- **msg.action** — which of the four actions to run. You can also put this in `msg.topic`
- **msg.payload** — the arguments for that action

<table>
<thead>
  <tr>
    <th>
      Action
    </th>
    
    <th>
      What it returns
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        fetch
      </code>
    </td>
    
    <td>
      Offsets for a topic and partition, up to <code>
        maxNum
      </code>
      
       of them
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        fetchEarliestOffsets
      </code>
    </td>
    
    <td>
      The oldest offsets still held for a topic
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        fetchLatestOffsets
      </code>
    </td>
    
    <td>
      The newest offsets for a topic, in other words the end of it
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        fetchCommits
      </code>
    </td>
    
    <td>
      The offsets a consumer group has committed
    </td>
  </tr>
</tbody>
</table>

Note the spelling of `fetchEarliestOffsets`: it is plural, and the node's own built-in help has it wrong. An action the node doesn't recognise comes out of the second output as `invalid msg action or topic`.

### Payloads

`fetch` takes a partition and a limit:

```json
[{ "topic": "test", "partition": 0, "maxNum": 100 }]
```

`fetchEarliestOffsets` and `fetchLatestOffsets` take the same payload as `fetch`, because all three run the same underlying request. The `time` field is what decides which end of the topic you get: `-2` for the earliest offset, `-1` for the latest.

```json
[{ "topic": "test", "partition": 0, "time": -2 }]
```

Leave `time` out and you get the offset as of now, which is the latest. So `fetchLatestOffsets` works with the short payload below, while `fetchEarliestOffsets` needs `"time": -2` to return what its name promises.

```json
[{ "topic": "test" }]
```

`fetchCommits` is the odd one out. Its payload is an object rather than an array, and it needs the group as well as the topics:

```json
{
  "groupid": "aGroup",
  "topics": [{ "topic": "test" }]
}
```

Note the spelling: `groupid`, all lower case.

### Measuring how far behind a consumer is

Run `fetchLatestOffsets` for the topic and `fetchCommits` for the group reading it, then subtract one from the other. The gap is how many messages the group still has to get through. Raise an alert when it grows past a threshold you are comfortable with.

A Kafka Consumer's own messages carry the same figures, if you would rather not poll: `msg._kafka.highWaterOffset` minus `msg._kafka.offset` is the lag at the moment that message was read.

These are also the offsets you put in a consumer's **Topic** table when you want to read a period of data again.

## Payloads are text

Kafka stores messages as bytes. With the default `utf8` encoding, a consumer gives you the message as text, and a producer expects text.

The nodes can handle the conversion for you, or you can do it in the flow:

- **Sending:** Kafka Producer already stringifies an object payload for you, so an object goes out as JSON text with no extra work. Turn on **Convert message from JSON** to stringify every payload, including strings and numbers, or put a [`json`](/docs/node-red/core-nodes/parsers/json/) node in stringify mode before the producer if you would rather control it in the flow.
- **Receiving:** turn on **Convert message to JSON** on Kafka Consumer, or put a `json` node in parse mode after it.

Kafka Consumer Group has no equivalent option, so use a `json` node there.

For a plain number, a `json` node is more than you need. A `change` node that sets `msg.payload` to the JSONata expression `$number(msg.payload)` is enough.

For binary payloads, set **Encoding** to `raw` on the consumer and skip the `json` node altogether.
