Kafka client configuration

Pass arbitrary consumer, producer, and restoration-consumer properties through to the Kafka clients, and understand which properties StoatFlow forces for correctness.

StoatFlow constructs its own Kafka consumer, producer, and restoration consumers. You configure those clients by passing standard Kafka client properties through your application.yaml under stoatflow.kafka.*. This page covers the passthrough form, the opinionated defaults StoatFlow layers on top, and the small set of properties it forces for exactly-once correctness.

Passthrough form

Every property you set under stoatflow.kafka.consumer, stoatflow.kafka.producer, stoatflow.kafka.restoration-consumer or stoatflow.kafka.admin is passed straight to the corresponding Kafka client, using the exact Kafka property names (dotted keys):

stoatflow:
  bootstrap-servers: localhost:9092
  kafka:
    consumer:
      max.poll.records: 1000
      auto.offset.reset: latest   # StoatFlow defaults this to earliest; override to latest here
      fetch.min.bytes: 65536
    producer:
      batch.size: 524288
      linger.ms: 50
      compression.type: lz4

You don't need to list every property — only the ones you want to change. Anything you omit falls back to StoatFlow's framework default, and anything StoatFlow doesn't set falls back to the Kafka client default.

These are standard Kafka client properties. Use the names from the Apache Kafka consumer and producer configuration reference verbatim. StoatFlow does not rename or alias them.

For where these keys sit in the wider configuration tree (and how YAML, environment variables, and system properties compose), see the configuration model and the core configuration reference.

How consumer properties resolve

Consumer properties are resolved by merging layers, each overwriting the previous:

LayerSourcePurpose
1Kafka client defaultsImplicit defaults from the Kafka client library
2Framework defaultsStoatFlow's opinionated defaults (see below)
3stoatflow.kafka.consumerYour overrides
4main.consumer. keysMain-consumer-only overrides — reachable only from a KS-keyed Properties, not YAML
5Forced overridesSet by the framework, cannot be overridden

Framework defaults

PropertyStoatFlow defaultKafka defaultRationale
max.poll.records500500Matches the Kafka default
fetch.max.bytes50 MiB50 MiBMatches the Kafka default
max.partition.fetch.bytes10 MiB1 MiBA single instance consumes all partitions; larger per-partition fetches reduce network round trips
auto.offset.resetearliestlatestMatches Kafka Streams (which also defaults its main consumer to earliest) — a fresh application.id processes the topic from the beginning. Overridable.
max.poll.interval.ms10 min5 minStoatFlow is single-instance, so a coordinator partition revocation is always fatal (no peer to rebalance to). Genuine hangs are already caught by the commit-pipeline watchdog (commit-stall-threshold-ms, 45 s) and the stall-aware liveness probe, so a short interval only causes spurious fatal shutdowns under transient slowness (GC pauses, CPU contention, a slow blocking downstream). A generous default removes that false-positive risk. Overridable.

Forced overrides

These are always set by the framework. Setting them under stoatflow.kafka.consumer has no effect.

PropertyValueReason
bootstrap.serversfrom stoatflow.bootstrap-serversMust match the cluster
group.idthe application IDStatic group membership for the single instance
group.instance.idthe application IDStatic membership (KIP-345) for instant partition re-assignment on restart
enable.auto.commitfalseOffsets are committed by StoatFlow's commit protocol, not by the Kafka client
isolation.levelread_committedexactly-once onlyAn exactly-once application must not ingest a transactional upstream's aborted batches. Matches Kafka Streams. Under at-least-once the key is not forced, so you can still set it yourself if you read a transactional upstream

auto.offset.reset defaults to earliest (matching Kafka Streams) rather than the raw Kafka client's latest. It is not forced — override it globally under stoatflow.kafka.consumer, or per source topic with Consumed.withOffsetResetPolicy(...), which seeks explicitly and takes precedence for that topic.

One exception: with hot-standby HA enabled, group.instance.id is omitted (and any user-set value removed) — HA pods use assign() with no consumer-group membership, so static membership does not apply.

How producer properties resolve

Producer properties resolve through the same layered merge:

LayerSourcePurpose
1Kafka client defaultsImplicit defaults from the Kafka client library
2Framework defaultsStoatFlow's opinionated defaults (see below)
3stoatflow.kafka.consumer security subsetThe consumer's security.* / ssl.* / sasl.* settings, so a secured cluster needs its credentials only once
4stoatflow.kafka.producerYour overrides
5Forced overridesSet by the framework, cannot be overridden
Shadow a security family completely, or not at all. Layer 4 overrides Layer 3 per key, so a producer block naming only some keys of a security. / ssl. / sasl. family inherits the rest and ends up mixed. A consumer on OAUTHBEARER plus a producer setting only sasl.mechanism: PLAIN and sasl.jaas.config still inherits the consumer's sasl.login.callback.handler.class, and the producer then fails to construct at startup. StoatFlow warns when it detects a partial shadow and names the inherited keys.

Framework defaults

PropertyStoatFlow defaultKafka defaultRationale
batch.size256 KiB16 KiBStoatFlow produces output across many keys at once; larger batches improve throughput
linger.ms200Wait up to 20 ms to fill batches — paired with the larger batch.size
retry.backoff.ms50100Halves the per-retry wait on the transaction coordinator's transient CONCURRENT_TRANSACTIONS response, shrinking the commit-latency tail at frequent commit cadences
buffer.memory256 MiB32 MiBHeadroom for the larger batches without producer-side backpressure
enable.idempotencetruetrueExplicit baseline locked to the StoatFlow version, insulating you from upstream default changes. Also forced in EOS mode (below).
acksallallExplicit baseline locked to the StoatFlow version. Also forced in EOS mode.
max.in.flight.requests.per.connection55Explicit baseline locked to the StoatFlow version. Also forced in EOS mode.
Memory note.buffer.memory (256 MiB) is producer-side buffering and is separate from RocksDB and state-store memory. On small-heap deployments (under ~2 GiB) with both heavy state writes and heavy sink output, consider reducing buffer.memory — see the memory-constrained scenario below and the tuning guide.

Forced overrides

PropertyValueReason
bootstrap.serversfrom stoatflow.bootstrap-serversMust match the cluster
retriesInteger.MAX_VALUEUnlimited retries are required for idempotent / transactional producer correctness
max.block.msthe configured barrier timeoutBounds how long a send() can block on a slow broker, so a broker slowdown surfaces as a timeout rather than a silent stall
delivery.timeout.msthe configured barrier timeoutBounds the end-to-end delivery path on the same budget

The barrier timeout is the stoatflow.commit-barrier.timeout-ms knob — see the core configuration reference.

Exactly-once mode only

When the processing guarantee is EXACTLY_ONCE, StoatFlow additionally forces the transactional producer properties:

PropertyValueReason
transactional.id{applicationId}-producerTransaction-coordination identity
transaction.timeout.msthe configured barrier timeoutMust align with the commit budget
enable.idempotencetrueRequired for the transactional producer — disabling it breaks exactly-once
acksallacks < all weakens durability under broker failures, which is incompatible with the exactly-once guarantee
max.in.flight.requests.per.connection5The maximum that still preserves the idempotent producer's ordering guarantee

In at-least-once mode (AT_LEAST_ONCE), enable.idempotence, acks, and max.in.flight.requests.per.connection stay at the framework-default layer and remain overridable via stoatflow.kafka.producer, so you can trade throughput against durability. In exactly-once mode they are forced: any value you set under stoatflow.kafka.producer is overwritten by the framework's forced override, so the client never sees a configuration that would break exactly-once. To see what the producer actually receives, check the resolved configuration (see Inspecting the resolved configuration below).

Restoration consumer

State stores recover from changelog topics on startup using dedicated restoration consumers, which are configured independently of the processing consumer under stoatflow.kafka.restoration-consumer. They inherit the processing stoatflow.kafka.consumer as a shared baseline, then layer restoration-specific defaults and overrides on top:

LayerSourcePurpose
1Kafka client defaultsImplicit defaults from the Kafka client library
2stoatflow.kafka.consumerProcessing-consumer config — shared baseline inherited by restoration
3Framework defaultsRestoration-specific defaults (see below)
4stoatflow.kafka.restoration-consumerYour restoration overrides
5Forced overridesSet by the framework, cannot be overridden

Framework defaults

Tuned for bulk changelog reads during recovery:

PropertyDefaultKafka defaultRationale
max.poll.records1,000500Larger batches reduce poll overhead during bulk reads
fetch.max.bytes50 MiB50 MiBMatches the Kafka default (already large)
max.partition.fetch.bytes10 MiB1 MiBFaster per-partition fetches during restoration

Forced overrides

PropertyValueReason
bootstrap.serversfrom stoatflow.bootstrap-serversMust match the cluster
group.id{applicationId}-restoration-{timestamp}Unique per restoration run
enable.auto.commitfalseRestoration offsets are tracked alongside the state, not by the Kafka client
auto.offset.resetearliestRestoration must read the changelog from the beginning
isolation.levelread_committed (EOS) / read_uncommitted (ALO)Must match the processing guarantee

Restoration tuning:

stoatflow:
  kafka:
    restoration-consumer:
      max.poll.records: 5000
      fetch.max.bytes: 104857600   # 100 MiB

Common tuning scenarios

High throughput

For maximum throughput with high event rates:

stoatflow:
  kafka:
    consumer:
      max.poll.records: 2000
      fetch.min.bytes: 65536        # 64 KiB — wait for larger fetches
      fetch.max.wait.ms: 200        # max wait for fetch.min.bytes
    producer:
      batch.size: 524288            # 512 KiB
      linger.ms: 50                 # allow more time to fill batches
      compression.type: lz4         # reduce network bandwidth

Low latency

For latency-sensitive workloads:

stoatflow:
  kafka:
    consumer:
      fetch.min.bytes: 1            # return immediately (Kafka default)
      max.poll.records: 100         # smaller batches for lower processing latency
    producer:
      linger.ms: 0                  # send immediately
      batch.size: 16384             # Kafka default (16 KiB)

Large records

When individual records are large (e.g. complex Avro > 10 KB):

stoatflow:
  kafka:
    consumer:
      max.partition.fetch.bytes: 20971520   # 20 MiB
      fetch.max.bytes: 104857600            # 100 MiB
    producer:
      max.request.size: 10485760            # 10 MiB
      buffer.memory: 536870912              # 512 MiB

Memory-constrained environments

When heap is limited:

stoatflow:
  kafka:
    consumer:
      max.poll.records: 100
      max.partition.fetch.bytes: 1048576    # 1 MiB (Kafka default)
      fetch.max.bytes: 10485760             # 10 MiB
    producer:
      batch.size: 32768                     # 32 KiB
      buffer.memory: 33554432               # 32 MiB (Kafka default)

Fast restoration

For large state stores where recovery time matters:

stoatflow:
  kafka:
    restoration-consumer:
      max.poll.records: 5000
      receive.buffer.bytes: 1048576         # 1 MiB socket buffer

Low-memory restoration

When restoration causes GC pressure on a constrained heap:

stoatflow:
  kafka:
    restoration-consumer:
      max.poll.records: 200
      max.partition.fetch.bytes: 1048576    # 1 MiB (Kafka default)
      fetch.max.bytes: 10485760             # 10 MiB

Setting properties via environment variables

Kafka client properties can also be set via environment variables, which take precedence over YAML. The format is STOATFLOW__KAFKA__{CONSUMER|PRODUCER|RESTORATION_CONSUMER|ADMIN}__{PROPERTY}, where the property name uses underscores instead of dots and is uppercased:

# Consumer properties
export STOATFLOW__KAFKA__CONSUMER__MAX_POLL_RECORDS=1000
export STOATFLOW__KAFKA__CONSUMER__FETCH_MAX_BYTES=104857600

# Producer properties
export STOATFLOW__KAFKA__PRODUCER__BATCH_SIZE=524288
export STOATFLOW__KAFKA__PRODUCER__LINGER_MS=50

# Restoration consumer properties
export STOATFLOW__KAFKA__RESTORATION_CONSUMER__MAX_POLL_RECORDS=5000

So max.poll.records becomes STOATFLOW__KAFKA__CONSUMER__MAX_POLL_RECORDS. See the configuration model for the full precedence rules.

SASL and SSL

Authentication and encryption are plain Kafka client properties — pass them through like any other:

Set them once, under consumer. StoatFlow propagates the consumer's security.* / ssl.* / sasl.* subset to every other client it builds — the producer, the admin clients (changelog topic management, broker health check, consumer-group preflight), the restoration consumers, and the HA metadata-log clients:

stoatflow:
  bootstrap-servers: broker-1:9093,broker-2:9093
  kafka:
    consumer:
      security.protocol: SASL_SSL
      sasl.mechanism: PLAIN
      sasl.jaas.config: ${KAFKA_SASL_JAAS_CONFIG}
      ssl.truststore.location: /etc/kafka/truststore.jks
      ssl.truststore.password: ${KAFKA_TRUSTSTORE_PASSWORD}

Keep secrets out of committed files — reference them with environment-variable placeholders (${...}) as shown.

If a particular client authenticates as a different principal, override it on that client's own map — stoatflow.kafka.producer, stoatflow.kafka.admin or stoatflow.kafka.restoration-consumer — which always wins over the inherited subset. Spell out every key of the family you are overriding: the override is per key, so anything you leave out is still inherited (see the warning above).

stoatflow:
  kafka:
    consumer:
      security.protocol: SASL_SSL
      sasl.mechanism: PLAIN
      sasl.jaas.config: ${KAFKA_SASL_JAAS_CONFIG}
    admin:
      # only when the admin principal differs from the consumer's
      sasl.jaas.config: ${KAFKA_ADMIN_SASL_JAAS_CONFIG}
Porting from Kafka Streams? A KS-keyed Properties normally spells these unprefixed (security.protocol at the top level, no consumer. prefix). That works: StoatFlow routes a bare client key to every client it is valid for, exactly as Kafka Streams does, with any prefixed spelling taking precedence. See the KS compatibility matrix.

Inspecting the resolved configuration

The merged application configuration is available at runtime from the /config HTTP endpoint, with sensitive values (passwords, secrets, tokens) masked. It serves JSON or YAML by content negotiation. See the REST API reference.

The resolved configuration is also logged at startup when the engine logger is at DEBUG:

logging:
  level:
    io.stoatflow.core.runtime.StreamProcessingEngine: DEBUG