Kafka Streams Windowing
Windowing groups records into finite sets based on time, enabling time-bounded aggregations and joins. This guide covers window types, time semantics, and late arrival handling.
Time Concepts
Section titled “Time Concepts”Time Types
Section titled “Time Types”| Time Type | Description | Use Case |
|---|---|---|
| Event time | Timestamp embedded in record | Business logic, reprocessing |
| Processing time | Wall-clock time when processed | Simple cases, debugging |
| Ingestion time | Time when record enters Kafka | Proxy for event time |
Configuring Time
Section titled “Configuring Time”// Extract event time from recordConsumed<String, Event> consumed = Consumed.with(Serdes.String(), eventSerde) .withTimestampExtractor(new TimestampExtractor() { @Override public long extract(ConsumerRecord<Object, Object> record, long partitionTime) { Event event = (Event) record.value(); return event.getTimestamp(); } });
KStream<String, Event> stream = builder.stream("events", consumed);Window Types
Section titled “Window Types”Tumbling Windows
Section titled “Tumbling Windows”Fixed-size, non-overlapping windows:
// 5-minute tumbling windowsKTable<Windowed<String>, Long> tumblingCounts = stream .groupByKey() .windowedBy(TimeWindows.ofSizeWithNoGrace(Duration.ofMinutes(5))) .count();
// With grace period for late arrivalsKTable<Windowed<String>, Long> tumblingWithGrace = stream .groupByKey() .windowedBy(TimeWindows.ofSizeAndGrace( Duration.ofMinutes(5), Duration.ofMinutes(1) // Accept late records up to 1 minute )) .count();Hopping Windows
Section titled “Hopping Windows”Fixed-size, overlapping windows:
// 5-minute windows, advancing every 2 minutesKTable<Windowed<String>, Long> hoppingCounts = stream .groupByKey() .windowedBy(TimeWindows.ofSizeAndGrace(Duration.ofMinutes(5), Duration.ofMinutes(1)) .advanceBy(Duration.ofMinutes(2))) .count();Sliding Windows
Section titled “Sliding Windows”Window around each record, used for joins:
// Join events within 5 minutes of each otherKStream<String, EnrichedOrder> joined = orders.join( payments, (order, payment) -> new EnrichedOrder(order, payment), JoinWindows.ofTimeDifferenceWithNoGrace(Duration.ofMinutes(5)), StreamJoined.with(Serdes.String(), orderSerde, paymentSerde));
// Sliding windows for aggregations (Kafka 2.7+)KTable<Windowed<String>, Long> slidingCounts = stream .groupByKey() .windowedBy(SlidingWindows.ofTimeDifferenceAndGrace( Duration.ofMinutes(5), Duration.ofMinutes(1) )) .count();Session Windows
Section titled “Session Windows”Dynamic windows based on activity gaps:
// Session windows with 5-minute inactivity gapKTable<Windowed<String>, Long> sessionCounts = stream .groupByKey() .windowedBy(SessionWindows.ofInactivityGapAndGrace( Duration.ofMinutes(5), Duration.ofMinutes(1) )) .count();Window Comparison
Section titled “Window Comparison”| Window Type | Size | Overlap | Use Case |
|---|---|---|---|
| Tumbling | Fixed | None | Periodic reports |
| Hopping | Fixed | Yes | Smoothed metrics |
| Sliding | Fixed | Yes | Correlation analysis |
| Session | Variable | None | User sessions |
Late Arrivals
Section titled “Late Arrivals”Grace Period
Section titled “Grace Period”Configure how long to accept late records:
// Accept records up to 1 minute lateTimeWindows.ofSizeAndGrace( Duration.ofMinutes(5), Duration.ofMinutes(1));
// No grace - reject all late records (default)TimeWindows.ofSizeWithNoGrace(Duration.ofMinutes(5));Handling Late Records
Section titled “Handling Late Records”Suppression
Section titled “Suppression”Control when windowed results are emitted:
// Emit only final resultsKTable<Windowed<String>, Long> finalCounts = stream .groupByKey() .windowedBy(TimeWindows.ofSizeAndGrace(Duration.ofMinutes(5), Duration.ofMinutes(1))) .count() .suppress(Suppressed.untilWindowCloses(BufferConfig.unbounded()));
// Emit final results with bounded bufferKTable<Windowed<String>, Long> boundedFinal = stream .groupByKey() .windowedBy(TimeWindows.ofSizeAndGrace(Duration.ofMinutes(5), Duration.ofMinutes(1))) .count() .suppress(Suppressed.untilWindowCloses( BufferConfig.maxBytes(1_000_000L) // 1MB buffer .shutDownWhenFull() ));
// Emit intermediate results with rate limitingKTable<Windowed<String>, Long> rateLimited = stream .groupByKey() .windowedBy(TimeWindows.ofSizeAndGrace(Duration.ofMinutes(5), Duration.ofMinutes(1))) .count() .suppress(Suppressed.untilTimeLimit( Duration.ofSeconds(30), BufferConfig.unbounded() ));Windowed Aggregations
Section titled “Windowed Aggregations”KTable<Windowed<String>, Long> windowedCounts = stream .groupByKey() .windowedBy(TimeWindows.ofSizeAndGrace(Duration.ofMinutes(5), Duration.ofMinutes(1))) .count( Materialized.<String, Long, WindowStore<Bytes, byte[]>>as("windowed-counts") .withRetention(Duration.ofHours(1)) );Reduce
Section titled “Reduce”KTable<Windowed<String>, Double> maxValues = stream .groupByKey() .windowedBy(TimeWindows.ofSizeAndGrace(Duration.ofMinutes(5), Duration.ofMinutes(1))) .reduce( (v1, v2) -> Math.max(v1, v2), Materialized.with(Serdes.String(), Serdes.Double()) );Aggregate
Section titled “Aggregate”KTable<Windowed<String>, Statistics> stats = stream .groupByKey() .windowedBy(TimeWindows.ofSizeAndGrace(Duration.ofMinutes(5), Duration.ofMinutes(1))) .aggregate( Statistics::new, (key, value, stats) -> stats.add(value), Materialized.<String, Statistics, WindowStore<Bytes, byte[]>>as("stats-store") .withValueSerde(statisticsSerde) );Windowed Keys
Section titled “Windowed Keys”Working with Windowed Keys
Section titled “Working with Windowed Keys”KTable<Windowed<String>, Long> windowedCounts = /* ... */;
// Convert to stream for further processingKStream<Windowed<String>, Long> countsStream = windowedCounts.toStream();
// Extract window informationcountsStream.foreach((windowedKey, count) -> { String key = windowedKey.key(); Window window = windowedKey.window(); long start = window.start(); long end = window.end();
System.out.printf("Key: %s, Window: [%d, %d), Count: %d%n", key, start, end, count);});
// Change key to include window infoKStream<String, Long> flatCounts = countsStream.map((windowedKey, count) -> { String newKey = windowedKey.key() + "-" + windowedKey.window().start(); return KeyValue.pair(newKey, count);});Windowed Key Serialization
Section titled “Windowed Key Serialization”// For output topicscountsStream.to( "windowed-counts", Produced.with( WindowedSerdes.timeWindowedSerdeFrom(String.class, Duration.ofMinutes(5).toMillis()), Serdes.Long() ));
// Custom windowed key serdeSerde<Windowed<String>> windowedSerde = new WindowedSerdes.TimeWindowedSerde<>( Serdes.String(), Duration.ofMinutes(5).toMillis());Retention Configuration
Section titled “Retention Configuration”Store Retention
Section titled “Store Retention”// Materialized store with retentionMaterialized.<String, Long, WindowStore<Bytes, byte[]>>as("windowed-store") .withRetention(Duration.ofHours(24)); // Keep windows for 24 hours
// Retention must be >= window size + grace period// retention >= size + graceChangelog Retention
Section titled “Changelog Retention”// Configure changelog topic retentionprops.put(StreamsConfig.topicPrefix("windowstore.changelog") + "retention.ms", String.valueOf(Duration.ofHours(24).toMillis()));Querying Windowed Stores
Section titled “Querying Windowed Stores”Point Query
Section titled “Point Query”ReadOnlyWindowStore<String, Long> store = streams.store( StoreQueryParameters.fromNameAndType( "windowed-counts", QueryableStoreTypes.windowStore() ));
// Fetch for specific windowInstant windowStart = Instant.now().minus(Duration.ofMinutes(5));Long count = store.fetch("key", windowStart.toEpochMilli());Range Query
Section titled “Range Query”// Fetch all windows in time rangeInstant from = Instant.now().minus(Duration.ofHours(1));Instant to = Instant.now();
try (WindowStoreIterator<Long> iter = store.fetch("key", from, to)) { while (iter.hasNext()) { KeyValue<Long, Long> kv = iter.next(); long windowStart = kv.key; long count = kv.value; System.out.printf("Window starting %d: count=%d%n", windowStart, count); }}
// Fetch all keys in time rangetry (KeyValueIterator<Windowed<String>, Long> iter = store.fetchAll(from, to)) { while (iter.hasNext()) { KeyValue<Windowed<String>, Long> kv = iter.next(); // Process each windowed key-value }}Best Practices
Section titled “Best Practices”Window Size Selection
Section titled “Window Size Selection”| Consideration | Guidance |
|---|---|
| Latency requirements | Smaller windows = faster results |
| Data volume | Larger windows = fewer outputs |
| Late arrivals | Grace period adds latency |
| Memory usage | More windows = more memory |
Performance Optimization
Section titled “Performance Optimization”| Practice | Recommendation |
|---|---|
| Limit retention | Keep only necessary history |
| Use suppression | Reduce output volume |
| Appropriate grace | Balance completeness vs latency |
| Monitor state size | Alert on excessive growth |
Common Patterns
Section titled “Common Patterns”// Metrics per minute with 1-hour retentionTimeWindows.ofSizeAndGrace(Duration.ofMinutes(1), Duration.ofSeconds(30)) // Materialized with 1-hour retention
// User sessions with 30-minute timeoutSessionWindows.ofInactivityGapAndGrace(Duration.ofMinutes(30), Duration.ofMinutes(5))
// Sliding average over 5-minute windowSlidingWindows.ofTimeDifferenceAndGrace(Duration.ofMinutes(5), Duration.ofMinutes(1))Related Documentation
Section titled “Related Documentation”- Kafka Streams Overview - Stream processing concepts
- DSL Reference - Stream operations
- State Stores - State management
- Error Handling - Exception handling