Client for consuming messages from Kafka topics with support for consumer groups and transactional message isolation.
The consumer inherits from the Base client.
The complete TypeScript type of the Consumer is determined by the deserializers option.
| Name | Payload Type | Description |
|---|---|---|
consumer:group:join |
ConsumerGroupJoinPayload |
Emitted when joining a group. |
consumer:group:leave |
ConsumerGroupLeavePayload |
Emitted when leaving a group. |
consumer:group:rejoin |
(none) | Emitted when re-joining a group after a rebalance. |
consumer:group:rebalance |
ConsumerGroupRebalancePayload |
Emitted when group rebalancing occurs. |
consumer:group:autocommit:error |
ConsumerGroupAutocommitErrorPayload |
Emitted when autocommit fails during a cooperative rebalance. The rebalance still proceeds. |
consumer:heartbeat:start |
ConsumerHeartbeatPayload? |
Emitted when starting new heartbeats. |
consumer:heartbeat:cancel |
ConsumerHeartbeatPayload |
Emitted if a scheduled heartbeat has been canceled. |
consumer:heartbeat:end |
ConsumerHeartbeatPayload? |
Emitted during successful heartbeats. |
consumer:heartbeat:error |
ConsumerHeartbeatErrorPayload |
Emitted during failed heartbeats. |
consumer:heartbeat:stalled |
ConsumerHeartbeatStalledPayload |
Emitted once per stall episode when no heartbeat has succeeded within heartbeatStallTimeout. Heartbeat handling is unchanged after the event; a successful heartbeat re-arms the tracking. |
consumer:lag |
Offsets |
Emitted during periodic lag monitoring with calculated lag data for topics and partitions. |
consumer:lag:error |
Error |
Emitted when lag calculation fails during monitoring operations. |
Creates a new consumer with type Consumer<Key, Value, HeaderKey, HeaderValue>.
Options:
| Property | Type | Default | Description |
|---|---|---|---|
| groupId | string |
Consumer group ID. | |
| groupInstanceId | string |
Consumer group instance ID. | |
| autocommit | boolean | number |
true |
Whether to autocommit consumed messages. If it is true, then messages are committed immediately.If it is a number, it specifies how often offsets will be committed. Only the last offset for a topic-partition is committed. If set to false, then each message read from the stream will have a commit method which should be used to manually commit offsets. |
| minBytes | number |
1 |
Minimum amount of data the brokers should return. The value might not be respected by Kafka. |
| maxBytes | number |
50MB | Maximum amount of data the brokers should return. The value might not be respected by Kafka. |
| maxBytesPerPartition | number |
Maximum amount of data the broker should return for each partition in a stream fetch request. If not set, it defaults to maxBytes. The value might not be respected by Kafka. |
|
| maxWaitTime | number |
5 seconds | Maximum amount of time in milliseconds the broker will wait before sending a response to a fetch request. |
| isolationLevel | number |
1 |
Kind of isolation applied to fetch requests. It can be used to only read producers-committed messages. The valid values are defined in the FetchIsolationLevels enumeration. |
| deserializers | Deserializers<Key, Value, HeaderKey, HeaderValue> |
Object that specifies which deserialisers to use. The object should only contain one or more of the key, value, headerKey and headerValue properties.Note: Should not be provided when using a registry. |
|
| beforeDeserialization | BeforeDeserializationHook |
Hook function called before deserialization of each message component (key, value, headers). Experimental: Does not follow semver and may change in minor/patch releases. Note: Should not be provided when using a registry. |
|
| registry | AbstractSchemaRegistry<Key, Value, HeaderKey, HeaderValue> |
Schema registry instance for automatic deserialization with schema management. See the Confluent Schema Registry guide for details. Experimental: Does not follow semver and may change in minor/patch releases. Note: When provided, do not use deserializers or beforeDeserialization. |
|
| highWaterMark | number |
1024 |
The maximum amount of messages to store in memory before delaying fetch requests. Note that this severely impacts both performance at the cost of memory use. |
| sessionTimeout | number |
1 minute | Amount of time in milliseconds to wait for a consumer to send the heartbeat before considering it down. This is only relevant when Kafka creates a new group. Not supported for groupProtocol=consumer, instead it is set with broker configuration property group.consumer.session.timeout.ms. |
| rebalanceTimeout | number |
2 minutes | Amount of time in milliseconds to wait for a consumer to confirm the rebalancing before considering it down. This is only relevant when Kafka creates a new group. |
| heartbeatInterval | number |
3 seconds | Interval in milliseconds between heartbeats. Not supported for groupProtocol=consumer, instead it is set with the broker configuration property group.consumer.heartbeat.interval. |
| heartbeatStallTimeout | number |
rebalanceTimeout |
Amount of time in milliseconds without a successful heartbeat before consumer:heartbeat:stalled is emitted once for the stall episode. A rejoin extends the stall window by rebalanceTimeout instead of suppressing it, and a failed rejoin restarts the window. Retrying continues after the event and a successful heartbeat re-arms the tracking. |
| groupProtocol | 'classic' | 'consumer' |
'classic' |
Group protocol to use. Use 'classic' for the original consumer group protocol and 'consumer' for the new protocol introduced in KIP-848.The 'consumer' protocol provides server-side partition assignment and incremental rebalancing behavior. |
| groupRemoteAssignor | string |
null |
Server-side assignor to use for groupProtocol=consumer. Keep it unset to let the server select a suitable assignor for the group. Available assignors: 'uniform' or 'range'. |
| protocols | GroupProtocolSubscription[] |
roundrobin, version 1 |
Protocols used by this consumer group. Each protocol must be an object specifying the name, version and optionally metadata properties. Use { name: 'cooperative-sticky', version: 3 } to opt in to KIP-429 cooperative rebalancing. Not supported for groupProtocol=consumer. |
| protocolsMetadata | GroupProtocolsMetadataCallback |
Callback used to compute opaque protocol metadata before joining or rejoining a classic consumer group. The returned Buffer is sent as the subscription user_data for every configured protocol. Not supported for groupProtocol=consumer. |
|
| partitionAssigner | GroupPartitionsAssigner |
Client-side partition assignment strategy. Built-in assigners are roundRobinAssigner and cooperativeStickyAssigner. Not supported for groupProtocol=consumer, use groupRemoteAssignor instead. |
|
| partitionAssignerTopicsSelector | GroupPartitionsAssignerTopicsSelector |
Selects the topic metadata required by a custom partitionAssigner. By default, metadata for every member's subscribed topics is required. Return [] to request no topic metadata for assignment. Not supported for groupProtocol=consumer. |
|
| streamContext | unknown |
Default opaque user data for MessagesStream instances created by this consumer. It is forwarded to stream-owned ConnectionPool and Connection instances. Kafka never reads, mutates, or interprets this value. |
|
| leaveGroupTimeout | number |
Unset | Maximum time in milliseconds for the group leave step during close(), for either group protocol. Set to 0 to skip the leave request. When omitted, the existing requestTimeout applies. |
It also supports all the constructor options of Base.
The readonly streamContext getters expose the same opaque values on the consumer instance.
protocolsMetadata lets each member attach fresh, opaque metadata to its JoinGroup subscription. Kafka forwards these bytes without interpreting them. The group leader can read the bytes from each member in a custom partitionAssigner through member.metadata.
The callback is invoked once per join or rebalance, before sending JoinGroup. The consumer refreshes metadata for its currently tracked topics before invoking it.
type GroupProtocolsMetadataCallback = (
protocols: GroupProtocolSubscription[],
topics: string[],
metadata: ClusterMetadata,
callback: (error: Error | null, metadata?: Buffer) => void
) => Buffer | Promise<Buffer> | undefinedThe callback can be used in three styles:
- Return a
Bufferdirectly. - Return a
Promise<Buffer>. - Return
undefinedand invoke the callback with aBuffer.
If the resolved value is not a Buffer, joinGroup() fails. Do not mix styles; if multiple paths settle, only the first result is used.
Example:
const consumer = new Consumer({
groupId: 'my-group',
clientId: 'my-consumer',
bootstrapBrokers: ['localhost:9092'],
protocolsMetadata (_protocols, topics, metadata) {
const userData: Record<string, number[]> = {}
for (const topic of topics) {
userData[topic] = metadata.topics.get(topic)!.partitions.map((_partition, partitionId) => partitionId)
}
return Buffer.from(JSON.stringify(userData), 'utf8')
},
partitionAssigner (_current, members, topics, metadata) {
for (const member of members.values()) {
const userData = JSON.parse(member.metadata!.toString('utf8'))
// Use the member-provided opaque metadata while computing assignments.
}
// Return GroupPartitionsAssignments[].
return []
}
})For classic groups with a custom partitionAssigner, partitionAssignerTopicsSelector(leader, members) selects which topics the leader must describe before computing assignments. leader is the leader's ExtendedGroupProtocolSubscription; members is the same map of member IDs to subscriptions passed to partitionAssigner. Both include the members' subscribed topics and opaque join metadata.
Return an array of topic names; every selected topic must have metadata available or the join fails. The assigner's metadata.topics contains only the selected topics. Return [] to request no topic metadata for assignment and pass an empty metadata.topics map. The client may still fetch cluster metadata if it is not cached. For example, partitionAssignerTopicsSelector: leader => leader.topics ?? [] requests only the leader's topics, while partitionAssignerTopicsSelector: () => [] lets the assigner rely entirely on join metadata. Without this callback, all subscribed topics are required as before. The callback can be set in consumer options or passed to joinGroup().
Classic consumer groups can opt in to KIP-429 cooperative sticky assignment:
import { COOPERATIVE_STICKY_ASSIGNOR, Consumer } from '@platformatic/kafka'
const consumer = new Consumer({
groupId: 'my-group',
bootstrapBrokers: ['localhost:9092'],
protocols: [{ name: COOPERATIVE_STICKY_ASSIGNOR, version: 3 }]
})The cooperative sticky assignor preserves existing ownership where possible and performs partition transfers over a follow-up rebalance so partitions are revoked before another member starts consuming them. All members of the group should advertise the same assignor during the migration.
Subscription metadata includes rackId when using protocol version 3, but the built-in assignor does not yet use rack information for rack-aware partition placement.
Returns true if the consumer is not closed and it is currently an active member of a consumer group.
This method will return false during consumer group rebalancing.
Creates a stream to consume messages from topics.
The return value is a stream of type MessagesStream<Key, Value, HeaderKey, HeaderValue>.
Creating new Consumer(...) does not start consuming yet. Group join/rejoin and actual fetch/start happen when consume() is called and a stream is created.
Each MessagesStream maintains its own fetch connection pool. This allows multiple active streams from the same consumer to fetch in parallel without head-of-line blocking each other.
Options:
| Property | Type | Default | Description |
|---|---|---|---|
topics |
string[] |
Topics to consume. | |
mode |
string |
LATEST |
Where to start fetching new messages. The valid values are defined in the MessagesStreamModes enumeration (see below). |
fallbackMode |
string |
LATEST |
Where to start fetching new messages when offset information is lacking for the consumer group. The valid values are defined in the MessagesStreamFallbackModes enumeration (see below). |
maxFetches |
number |
0 |
The maximum number of fetches to perform on each partition before automatically closing the stream. Setting to zero will disable automating closing. Note that Array.fromAsync can be used to create an array out of a stream. |
offsets |
TopicWithPartitionAndOffset[] |
When manual offset mode is specified, a list of topic-partition-offset triplets. | |
onDeserializationError |
DeserializationErrorHandler |
A callback invoked if a deserialiser throws. It receives a context object containing the original thrown value, payload type, raw record, actual offset and timestamp, and commit function. Return DeserializationErrorActions.SKIP to continue or DeserializationErrorActions.FAIL to destroy the stream. |
|
onCorruptedMessage |
CorruptedMessageHandler |
Deprecated: Use onDeserializationError instead. A callback that will be invoked if a deserialiser throws. The original thrown value is passed unchanged as the final error argument and is typed as unknown. If the function returns true, then the stream will throw an error and thus it will be destroyed. Note that the stream will not wait for the commit function to finish so make sure you handle callbacks and promises accordingly. |
|
context |
unknown |
Opaque user data for the created MessagesStream. It is forwarded to the stream-owned ConnectionPool and Connection instances. Kafka never reads, mutates, or interprets this value. When provided, it overrides the consumer constructor streamContext for that stream. |
It also accepts all options of the constructor except deserializers and groupId.
For consume(), maxBytesPerPartition controls the per-partition fetch limit used by the created MessagesStream. If omitted, it uses the consumer constructor maxBytesPerPartition; if that is also omitted, it uses the effective maxBytes value for the consume() call.
Use onDeserializationError to inspect a failed deserialisation and decide whether to skip the record or fail the stream:
import { DeserializationErrorActions, SchemaValidationError } from '@platformatic/kafka'
const stream = await consumer.consume({
topics: ['events'],
onDeserializationError ({ error, topic, partition, offset, payloadType }) {
if (error instanceof SchemaValidationError) {
console.error('Schema-invalid message', {
topic,
partition,
offset,
payloadType,
validationErrors: error.validationErrors
})
return DeserializationErrorActions.SKIP
}
return DeserializationErrorActions.FAIL
}
})The callback is synchronous and must return one of:
| Action | Behaviour |
|---|---|
FAIL |
The stream is destroyed with a UserError whose cause is the original error. |
SKIP |
The record is not emitted and consumption continues. |
CONTINUE |
The record is emitted in a degraded form and consumption continues. |
With autocommit enabled, the offset of a skipped or continued record remains eligible for commit.
onDeserializationError and the deprecated onCorruptedMessage cannot be configured together.
CONTINUE trades strict schema enforcement for availability: neither the message nor the stream is
lost when a payload cannot be decoded, which is what you want when a Schema Registry is temporarily
unreachable or when a subset of the messages is undecodable.
The delivered message keeps whatever was deserialized before the failure and carries the original bytes for the payload which failed and for the ones which were never attempted, since deserialization proceeds through headers, key and then value. The failure itself is reported on the metadata:
const stream = await consumer.consume({
topics: ['events'],
onDeserializationError () {
return DeserializationErrorActions.CONTINUE
}
})
for await (const message of stream) {
const failure = message.metadata.deserializationError
if (failure) {
// message.value is the raw Buffer, failure.payloadType says which payload failed
deadLetters.send({ topic: 'events-dlq', value: message.value })
continue
}
handle(message.value)
}Because the payload types are no longer guaranteed, always check
message.metadata.deserializationError before trusting key, value or headers.
Nothing is logged when a message is continued: at scale that would be a log storm. The error is
published on the plt:kafka:consumer:receives diagnostic channel and exposed on
the message metadata, so applications can report it at whatever rate they choose.
MessagesStreamModes modes:
| Name | Description |
|---|---|
LATEST |
Consume messages received after the started streaming. |
EARLIEST |
Consume messages from the earliest available on the broker. |
COMMITTED |
Resume the consumer group from the offset after the last committed offset. |
MANUAL |
Consume messages after the offsets provided in the offsets option. |
MessagesStreamFallbackModes modes:
| Name | Description |
|---|---|
LATEST |
Consume messages received after the started streaming. |
EARLIEST |
Consume messages from the earliest available on the broker. |
FAIL |
Throw an exception. |
Closes the consumer and all its connections.
If force is not true, then the method will throw an error if any MessagesStream created from this consumer is still active.
If force is true, then the method will close all MessagesStream created from this consumer.
The return value is void.
The leaveGroupTimeout constructor option bounds only the group leave step, after active streams have been closed. It accepts a non-negative number of milliseconds and applies to both group protocols. When omitted, the existing requestTimeout applies.
On timeout, the consumer clears its local group state and closes its connections without waiting for the broker. Setting leaveGroupTimeout to 0 skips the leave request entirely. The broker will expire the membership according to its session timeout if it does not receive the leave request. This option does not change other request timeouts or explicit leaveGroup() calls.
⚠️ Experimental API Confluent Schema Registry support and theregistry/beforeDeserializationintegration are experimental. They do not follow semver and may change in minor/patch releases.
The consumer supports automatic deserialization through schema registries like Confluent Schema Registry. When using a schema registry, messages are automatically deserialized according to their schemas that are fetched based on schema IDs embedded in the message.
Example with Confluent Schema Registry:
import { Consumer } from '@platformatic/kafka'
import { ConfluentSchemaRegistry } from '@platformatic/kafka/registries'
// Create a schema registry instance
const registry = new ConfluentSchemaRegistry({
url: 'http://localhost:8081',
auth: {
username: 'user',
password: 'password'
}
})
// Create a consumer with the registry
const consumer = new Consumer({
groupId: 'my-consumer-group',
clientId: 'my-consumer',
bootstrapBrokers: ['localhost:9092'],
registry // Registry handles deserialization automatically
})
// Create a consumer stream
const stream = await consumer.consume({
topics: ['events'],
autocommit: true
})
// Messages are automatically deserialized according to their schemas
for await (const message of stream) {
console.log('Key:', message.key) // Deserialized key object
console.log('Value:', message.value) // Deserialized value object
}
await consumer.close()When using a schema registry:
- Do not provide the
deserializersoption - the registry provides its own deserializers - Do not provide the
beforeDeserializationhook - the registry provides its own hook - Messages are automatically deserialized according to their schemas
- Schema IDs are extracted from the Confluent wire format (5-byte header)
- The registry fetches and caches schemas as needed
- JSON schemas are validated on receive (and optionally on send)
- Supports AVRO, Protocol Buffers, and JSON Schema formats
- The schema IDs used to decode each message are exposed as
message.metadata.schemas - The original key and value byte lengths are exposed as
message.metadata.lengths
For more details, see the Confluent Schema Registry documentation.
Deserializers and beforeDeserialization hooks receive the message being processed and can assign a
metadata object to it. Those properties are merged into the metadata of the message pushed to the
stream, on top of the consumer information the stream always adds:
const consumer = new Consumer({
groupId: 'my-consumer-group',
clientId: 'my-consumer',
bootstrapBrokers: ['localhost:9092'],
deserializers: {
...stringDeserializers,
value (data, headers, message) {
message.metadata = { valueLength: data.length }
return stringDeserializers.value(data)
}
}
})
for await (const message of stream) {
console.log(message.metadata.valueLength)
}Each message gets its own metadata object, so mutating it never affects the other messages of the same batch.
MessagesStream can be combined with the hwp batch operator when an
application needs to process multiple messages together:
import { batchIterator } from 'hwp'
const stream = await consumer.consume({
autocommit: false,
topics: ['events']
})
try {
for await (const batch of batchIterator(stream[Symbol.asyncIterator](), 100, 1000)) {
await processBatch(batch)
const offsets = new Map()
for (const message of batch) {
offsets.set(`${message.topic}:${message.partition}`, {
topic: message.topic,
partition: message.partition,
offset: message.offset + 1n,
leaderEpoch: message.leaderEpoch
})
}
await consumer.commit({ offsets: [...offsets.values()] })
}
} finally {
try {
await stream.close()
} finally {
await consumer.close(true)
}
}The batch is released when it reaches the maximum size or when the timeout expires. Since Kafka offsets are tracked per topic-partition, commit the highest successfully processed offset plus one for each partition in the batch. If processing fails before the commit, the uncommitted messages can be delivered again.
The complete runnable example is available in examples/batching. Run it with:
KAFKA_BROKER=localhost:9092 ./scripts/node examples/batching/index.tsBatch consumption will not be implemented as a separate API in the client. Keeping batching as an operator over the existing stream avoids duplicating stream lifecycle and offset semantics while allowing applications to choose their batch size, timeout and processing strategy.
When you set the clientRack option, the consumer sends it as the Fetch request rack_id and honors any preferred_read_replica the broker returns. Subsequent fetches for that partition route to the preferred replica until the lease expires, the metadata changes, or the preferred fetch fails. This applies to both the MessagesStream fetch loop and direct Consumer.fetch calls when every requested partition resolves to the same target broker. Otherwise, the original node is used as a fallback.
The lease length matches the consumer's metadataMaxAge. Cached preferred replicas are also dropped when:
- the broker returns
preferred_read_replica = -1is treated as "no new hint"; an existing cached value is kept until it expires - the cached node is no longer in the partition replicas, is offline, or is missing from cluster metadata (also triggers a metadata refresh)
- the partition is no longer assigned to this consumer after a rebalance
- a fetch request fails (the cache is cleared for every partition in the failed request, mirroring the Apache Kafka Java client)
For closest-replica fetching to actually pick a same-rack follower, the cluster must support KIP-392 (Apache Kafka 2.4+) and have a replica selector configured (for example, replica.selector.class=org.apache.kafka.common.replica.RackAwareReplicaSelector). Without this, the broker will not advertise a preferred replica and the consumer will continue to read from partition leaders.
The consumer manages group participation automatically. Some of the APIs are exposed to allow for advanced uses.
Fetches messages directly from a specific node without allocating a stream.
The return value is the raw response from Kafka.
Options:
| Property | Type | Default | Description |
|---|---|---|---|
| node | number | Node to consume data from. | |
| topics | FetchRequestTopic[] |
Topic and partitions to consume. | |
| minBytes | number |
1 |
Minimum amount of data the brokers should return. The value might not be respected by Kafka. |
| maxBytes | number |
50MB | Maximum amount of data the brokers should return. The value might not be respected by Kafka. This is a key throughput and memory tuning parameter: increase it to allow larger fetch responses and reduce fetch round trips, or decrease it to cap response size and memory pressure. |
| maxWaitTime | number |
5 seconds | Maximum amount of time in milliseconds the broker will wait before sending a response to a fetch request. |
| isolationLevel | number |
1 |
Kind of isolation applied to fetch requests. It can be used to only read producers-committed messages. The valid values are defined in the FetchIsolationLevels enumeration. |
Note that all the partitions must be hosted on the node otherwise the operation will fail.
Commits offsets for consumed messages.
If a commit requires rejoining the group, it is retried only if every requested partition is still assigned to this consumer. Otherwise, the original commit error is returned (PLT_KFK_PROTOCOL, or PLT_KFK_RESPONSE for aggregated broker errors), without resending any offsets under the new generation. This also applies to in-flight stream commits.
The return value is void.
Options:
| Property | Type | Description |
|---|---|---|
| offsets | CommitOptionsPartition[] |
Offsets to commit. Each offset is an object containing the topic, partition, offset and leaderEpoch properties. |
Lists available offsets for topics.
If the topics list is empty, then it will fetch the offsets of all topics currently consumed by the consumer.
The return value is a map where keys are in the form $topic:$partition and values are arrays of offsets (where the position represents the partition).
Options:
| Property | Type | Description |
|---|---|---|
| topics | string[] |
Topics to check. |
| partition | Record<string, number[]> |
Partitions to get for each topic. By default it fetches all the partitions of the topic. |
| timestamp | bigint |
Timestamp of the offsets to retrieve. If it is -1, it will return the last available offset; if it is -2, it will return the first available offset.These special values are also defined in the ListOffsetTimestamps enum. |
| isolationLevel | number |
Kind of isolation applied to fetch requests. It can be used to only read producers-committed messages. The valid values are defined in the FetchIsolationLevels enumeration. |
Lists offsets committed by the consumer group.
The return value is a map where keys are in the form $topic:$partition and values are arrays of offsets (where the position represents the partition).
Options:
| Property | Type | Description |
|---|---|---|
| topics | string[] |
Topics to check. |
Calculates the consumer lag for specified topics.
If the topics list is empty, then it will calculate the lag of all topics currently consumed by the consumer.
The return value is a map where keys are topic names and values are arrays of lag values (where the position represents the partition). A value of -1n indicates that the consumer is not assigned to that partition.
If a partition is filtered out via the partitions option, then a -2n will be returned for that partition.
Options:
| Property | Type | Description |
|---|---|---|
| topics | string[] |
Topics to check lag for. |
| partitions | Record<string, number[]> |
Partitions to get for each topic. By default it fetches all the partitions of the topic. |
Initiates periodic consumer lag monitoring at the specified interval in milliseconds.
Consumer lag data is automatically emitted through consumer:lag events at each monitoring cycle.
Monitoring continues until explicitly stopped with stopLagMonitoring or automatically terminates when the consumer closes.
The options parameter accepts the same configuration as getLag.
Terminates the periodic consumer lag monitoring that was previously activated with startLagMonitoring.
Finds the coordinator for the consumer group.
The return value is the number of the node acting as a coordinator for the group.
Joins (and creates if necessary) a consumer group.
It returns the group member ID for this consumer.
This method is no-op for groupProtocol=consumer.
Options:
| Property | Type | Default | Description |
|---|---|---|---|
| sessionTimeout | number |
1 minute | Amount of time in milliseconds to wait for a consumer to send the heartbeat before considering it down. This is only relevant when Kafka creates a new group. |
| rebalanceTimeout | number |
2 minutes | Amount of time in milliseconds to wait for a consumer to confirm the rebalancing before considering it down. This is only relevant when Kafka creates a new group. |
| heartbeatInterval | number |
3 seconds | Interval in milliseconds between heartbeats. |
| heartbeatStallTimeout | number |
rebalanceTimeout |
Amount of time in milliseconds without a successful heartbeat before consumer:heartbeat:stalled is emitted once for the stall episode. |
| protocols | GroupProtocolSubscription[] |
roundrobin, version 1 |
Protocols used by this consumer group. Each protocol must be an object specifying the name, version and optionally metadata properties. Use { name: 'cooperative-sticky', version: 3 } to opt in to KIP-429 cooperative rebalancing. |
| protocolsMetadata | GroupProtocolsMetadataCallback |
Callback used to compute opaque protocol metadata before joining or rejoining a classic consumer group. See Protocol metadata callback. | |
| partitionAssigner | GroupPartitionsAssigner |
Client-side partition assignment strategy for classic groups. | |
| partitionAssignerTopicsSelector | GroupPartitionsAssignerTopicsSelector |
Selects required topic metadata for a custom assigner. See Custom assignment topic metadata. |
Leaves a consumer group.
If force is not true, then the method will throw an error if any MessagesStream created from this consumer is still active.
The return value is void.
This method is no-op for groupProtocol=consumer.
If you use a fixed consumer group ID (or one that is persisted between restarts), you might encounter a rebalance issue if the application is closed without properly shutting down the consumer or issuing a leaveGroup request.
This commonly happens during development (for example, when you terminate the process with Ctrl+C or use a file watcher), or if the application crashes in production.
When the application exits without leaving the group, Kafka still considers the consumer as potentially alive. Upon restarting with the same group ID but a different member ID, Kafka triggers a group rebalance. Since the previous member did not formally leave, Kafka waits for the full session timeout (1 minute by default) to determine whether the old member is still active.
Only after this timeout expires and the member is considered dead will Kafka complete the rebalance and assign partitions to the new instance. During this period, the restarted consumer cannot consume any messages, even if it is the only member in the group.
If you encounter this issue, there are two ways to easily avoid it:
- If don't really use consumer groups, just use a random consumer group (like crypto.randomUUID) as your group ID.
- If you use consumer groups, manually call
joinGroup, rather than relying onconsumeto handle it automatically.
Kafka requires consumers to regularly report their status using a dedicated heartbeat API. If a consumer fails to send heartbeats within the configured session timeout, Kafka considers it dead. When this happens, the consumer must rejoin the group, during which time it cannot consume messages. Moreover, all other group members must also rejoin due to the triggered rebalance, resulting in a temporary halt in message consumption across the group.
We've identified three common scenarios that can lead to missed heartbeats while consuming messages:
-
Long fetch durations – If the consumer fetches messages with a maxWaitTime that exceeds the sessionTimeout, heartbeats may not be processed—even if they are sent—because the fetch API is blocking. @platformatic/kafka solves this by using separate fetch connections per
MessagesStream, ensuring that other APIs (such as heartbeats) remain unaffected and allowing parallel fetches across streams. -
Complex asynchronous message processing – When user code performs long or delayed async tasks while handling messages, it may block the driver from sending timely heartbeats. If the client does not decouple fetching from processing, heartbeats can be lost. Solutions like KafkaJS require users to manually send heartbeats during processing, which is error-prone. In contrast, @platformatic/kafka fully decouples message fetching from processing to prevent this issue entirely.
-
CPU-intensive operations – In Node.js, blocking the event loop with heavy computations prevents any I/O operations, including heartbeats. If such operations are unavoidable, the recommended solution is to offload them to a separate thread using Node.js Worker Threads.
By default, @platformatic/kafka retries an operation three times with a waiting time of 250 milliseconds (these are the default of the retries and retryDelay options). This is to ensure that connection error are properly reported to the user. If the operation fails, the error event is emitted on the stream. After that, the stream is no longer usable, but the consumer still is (thanks to the internal decoupled architecture) and therefore you just have to invoke consume again to obtain a new stream and start processing messages again.
If you instead want it to handle reconnections automatically, you just have to change the values of retries to a higher value or true to set it to "infinite retries". Also, increase retryDelay to at least a second to avoid opening too many TCP connections in short period of time if the broker is down.
You can change those values in the Consumer constructor (and, similarly, to any other @platformatic/kafka client).
Some Kafka servers (such as those using JWT tokens) may return a successful exit code (0) even when invalid credentials are provided, storing authentication information only in auth bytes rather than the exit code.
In such cases, you can provide the sasl.authBytesValidator option to validate authentication information from the auth bytes response rather than relying on the exit code alone.
For JWT tokens we already provide saslOAuthBearer.jwtValidateAuthenticationBytes.