Kafka client configuration
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 or stoatflow.kafka.producer 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.
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:
| Layer | Source | Purpose |
|---|---|---|
| 1 | Kafka client defaults | Implicit defaults from the Kafka client library |
| 2 | Framework defaults | StoatFlow's opinionated defaults (see below) |
| 3 | stoatflow.kafka.consumer | Your overrides |
| 4 | Forced overrides | Set by the framework, cannot be overridden |
Framework defaults
| Property | StoatFlow default | Kafka default | Rationale |
|---|---|---|---|
max.poll.records | 500 | 500 | Matches the Kafka default |
fetch.max.bytes | 50 MiB | 50 MiB | Matches the Kafka default |
max.partition.fetch.bytes | 10 MiB | 1 MiB | A single instance consumes all partitions; larger per-partition fetches reduce network round trips |
auto.offset.reset | earliest | latest | Matches Kafka Streams (which also defaults its main consumer to earliest) — a fresh application.id processes the topic from the beginning. Overridable. |
Forced overrides
These are always set by the framework. Setting them under stoatflow.kafka.consumer has no effect.
| Property | Value | Reason |
|---|---|---|
bootstrap.servers | from stoatflow.bootstrap-servers | Must match the cluster |
group.id | the application ID | Static group membership for the single instance |
group.instance.id | the application ID | Static membership (KIP-345) for instant partition re-assignment on restart |
enable.auto.commit | false | Offsets are committed by StoatFlow's commit protocol, not by the Kafka client |
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:
| Layer | Source | Purpose |
|---|---|---|
| 1 | Kafka client defaults | Implicit defaults from the Kafka client library |
| 2 | Framework defaults | StoatFlow's opinionated defaults (see below) |
| 3 | stoatflow.kafka.producer | Your overrides |
| 4 | Forced overrides | Set by the framework, cannot be overridden |
Framework defaults
| Property | StoatFlow default | Kafka default | Rationale |
|---|---|---|---|
batch.size | 256 KiB | 16 KiB | StoatFlow produces output across many keys at once; larger batches improve throughput |
linger.ms | 20 | 0 | Wait up to 20 ms to fill batches — paired with the larger batch.size |
retry.backoff.ms | 50 | 100 | Halves the per-retry wait on the transaction coordinator's transient CONCURRENT_TRANSACTIONS response, shrinking the commit-latency tail at frequent commit cadences |
buffer.memory | 256 MiB | 32 MiB | Headroom for the larger batches without producer-side backpressure |
enable.idempotence | true | true | Explicit baseline locked to the StoatFlow version, insulating you from upstream default changes. Also forced in EOS mode (below). |
acks | all | all | Explicit baseline locked to the StoatFlow version. Also forced in EOS mode. |
max.in.flight.requests.per.connection | 5 | 5 | Explicit baseline locked to the StoatFlow version. Also forced in EOS mode. |
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
| Property | Value | Reason |
|---|---|---|
bootstrap.servers | from stoatflow.bootstrap-servers | Must match the cluster |
retries | Integer.MAX_VALUE | Unlimited retries are required for idempotent / transactional producer correctness |
max.block.ms | the configured barrier timeout | Bounds 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.ms | the configured barrier timeout | Bounds 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:
| Property | Value | Reason |
|---|---|---|
transactional.id | {applicationId}-producer | Transaction-coordination identity |
transaction.timeout.ms | the configured barrier timeout | Must align with the commit budget |
enable.idempotence | true | Required for the transactional producer — disabling it breaks exactly-once |
acks | all | acks < all weakens durability under broker failures, which is incompatible with the exactly-once guarantee |
max.in.flight.requests.per.connection | 5 | The 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:
| Layer | Source | Purpose |
|---|---|---|
| 1 | Kafka client defaults | Implicit defaults from the Kafka client library |
| 2 | stoatflow.kafka.consumer | Processing-consumer config — shared baseline inherited by restoration |
| 3 | Framework defaults | Restoration-specific defaults (see below) |
| 4 | stoatflow.kafka.restoration-consumer | Your restoration overrides |
| 5 | Forced overrides | Set by the framework, cannot be overridden |
Framework defaults
Tuned for bulk changelog reads during recovery:
| Property | Default | Kafka default | Rationale |
|---|---|---|---|
max.poll.records | 1,000 | 500 | Larger batches reduce poll overhead during bulk reads |
fetch.max.bytes | 50 MiB | 50 MiB | Matches the Kafka default (already large) |
max.partition.fetch.bytes | 10 MiB | 1 MiB | Faster per-partition fetches during restoration |
Forced overrides
| Property | Value | Reason |
|---|---|---|
bootstrap.servers | from stoatflow.bootstrap-servers | Must match the cluster |
group.id | {applicationId}-restoration-{timestamp} | Unique per restoration run |
enable.auto.commit | false | Restoration offsets are tracked alongside the state, not by the Kafka client |
auto.offset.reset | earliest | Restoration must read the changelog from the beginning |
isolation.level | read_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}__{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:
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}
producer:
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. Set the same properties on restoration-consumer if your changelog topics live on the same secured cluster (they normally do, so inheritance from stoatflow.kafka.consumer covers it automatically). The consumer's security.* / ssl.* / sasl.* settings are also propagated to StoatFlow's internal admin clients (changelog topic management, broker health check), so a secured cluster needs no separate admin configuration.
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
Defaults, adaptivity, and presets
Why a minimal StoatFlow config runs well — the opinionated defaults, the parts that self-tune within bounds you set, and the RocksDB presets and tuning knobs you can change.
The runtime
The batteries-included :runtime module — StoatFlowRuntime.fromConfig, the lifecycle (start / awaitTermination / stop), and what it adds over the bare :core engine.