Kafka Streamssuppress operator lets you control when results are emitted, specifically allowing you to hold back intermediate results and only emit the final state of a key.

Let’s see it in action. Imagine we have a stream of user click events, and we want to count how many times each user clicks within a 10-minute window. Without suppress, we’d get an update for every single click. With suppress, we only want to know the final count after the 10-minute window has passed and no more clicks for that user arrive.

StreamsBuilder builder = new StreamsBuilder();

KStream<String, Long> clicks = builder.stream("click-topic");

clicks
    .mapValues(value -> "user") // Assuming value is the user ID
    .groupByKey()
    .windowedBy(TimeWindows.of(Duration.ofMinutes(10)))
    .count(Materialized.as("click-counts-store"))
    .suppress(Suppressed.untilWindowCloses(Suppressed.BufferConfig.unbounded()))
    .toStream((key, value) -> key.key() + "-" + key.window().start()); // Flatten windowed key

clicks.to("final-click-counts-topic");

The core of this is the suppress operator. It takes a Suppressed strategy. Here, Suppressed.untilWindowCloses() tells Kafka Streams to wait until the end of the time window before emitting anything for a given key. The Suppressed.BufferConfig.unbounded() means we don’t have a memory limit on how many intermediate results we might hold onto before emitting the final one.

When a key’s window closes, and no more records for that key are expected within that window’s grace period, the suppress operator will emit the final aggregated value for that key-window. This is incredibly useful for avoiding noisy intermediate states and only acting on definitive outcomes.

The windowedBy(TimeWindows.of(Duration.ofMinutes(10))) part defines our sliding or tumbling window. In this case, it’s a 10-minute window. count(Materialized.as("click-counts-store")) performs the aggregation, storing the counts in a state store named "click-counts-store".

The toStream((key, value) -> key.key() + "-" + key.window().start()) is a common pattern after windowed operations. It unwraps the Windowed<K> key, giving you back a regular K (the user ID in this case) and appending the window’s start time to create a unique key for the output topic. This ensures that even if a user clicks within two different, overlapping 10-minute windows, we get distinct output records for each window’s final count.

The suppress operator is fundamentally about managing state and latency. It allows you to trade off real-time updates for cleaner, more decisive final results. It’s not just about when to emit, but what to emit – the quiescent state after a period of activity.

What most people don’t realize is that Suppressed.untilWindowCloses() isn’t just a simple timer; it’s coupled with the concept of "grace periods" for late-arriving records. If a record arrives within the window’s duration but after the window has technically closed (e.g., a record arriving at 10:05 for a window that closed at 10:00), it will still be processed. The suppress operator’s untilWindowCloses strategy implicitly uses the window’s grace period to decide when a window is truly "finished" and no more updates for it are expected.

The next logical step after controlling emission with suppress is often dealing with the downstream impact of these delayed emissions, such as managing idempotency or handling out-of-order processing in subsequent stages.

Want structured learning?

Take the full Kafka-streams course →