Build real-time data processing pipelines with Apache Kafka and Apache Flink. Covers event streams, windowing, exactly-once semantics, state management, and the patterns that make stream processing reliable at scale.
Real-time stream processing handles data flows that must be processed within milliseconds or seconds of arrival, rather than waiting for batch windows. This approach is essential when downstream systems require immediate responses to events, such as fraud detection, real-time recommendations, or live analytics dashboards.
To consume a stream of events, configure your processor to connect to a message broker or streaming platform. The typical pattern involves creating a consumer that reads from a topic or partition and applies transformations.
# Example using standard Kafka CLI tools
kafka-console-consumer.sh \
--bootstrap-server kafka.example.com:9092 \
--topic user-events \
--from-beginning \
--value-deserializer=org.apache.kafka.common.serialization.StringDeserializer
The --from-beginning flag ensures you read all available messages, not just new ones. Without it, consumers may miss events that arrived while they were offline.
Each consumer group tracks its progress through a topic via offsets. Mismanagement leads to duplicate processing or data loss.
# View current offset positions
kafka-run-class.sh kafka.tools.GetOffsetShell \
--broker-list kafka.example.com:9092 \
--topic user-events \
--time -1
The --time -1 flag fetches the latest offset for each partition. Use --time -2 to get the earliest available offset.
For systems that aggregate from multiple sources:
# Example consumer configuration
kafka-console-consumer.sh \
--bootstrap-server kafka.example.com:9092 \
--topic user-events,user-actions \
--group event-aggregator \
--value-deserializer=org.apache.kafka.common.serialization.StringDeserializer \
--property print.key=true
Use the --group flag to define a consumer group. Multiple consumers in the same group will split partitions among themselves.
Transformations occur within the processing pipeline, not as separate batch jobs. A stream processor must handle each event individually, maintaining state where necessary.
# Example Python-based stream processor
import json
def process_event(event_data):
try:
event = json.loads(event_data)
# Apply transformations
transformed = {
'user_id': event['user_id'],
'timestamp': event['timestamp'],
'processed_at': time.time(),
'event_type': event['type']
}
return json.dumps(transformed)
except Exception as e:
# Log failed events for later replay
print(f"Failed to process: {e}")
return None
Events that fail processing should be routed to a dead-letter queue, not dropped silently.
Stateful transformations require maintaining data between events:
// Example in Java using Kafka Streams
StreamsBuilder builder = new StreamsBuilder();
KStream<String, String> source = builder.stream("input-topic");
KTable<String, Long> counts = source
.mapValues(value -> value.toLowerCase())
.groupBy(KeyValue.pair("user", "action"))
.count();
counts.toStream().to("output-topic");
Stateful operations use RocksDB internally and require sufficient disk space. The --state.dir parameter controls where state is stored.
Stream processors must handle failures gracefully, ensuring no data is lost during processing interruptions.
Failed messages should be sent to a separate topic:
# Producer configuration for failed events
kafka-console-producer.sh \
--bootstrap-server kafka.example.com:9092 \
--topic failed-events \
--property parse.key=true \
--property key.separator=:
The --property parse.key=true flag enables key-based routing. When a transformation fails, the original event should be preserved in the dead-letter queue.
Ensure that processing the same event twice does not produce duplicate results:
# Example idempotent consumer
def process_with_idempotency(event_key, event_data):
# Check if we've already processed this event
if redis.exists(f"processed:{event_key}"):
return
# Process the event
result = transform_event(event_data)
# Mark as processed
redis.setex(f"processed:{event_key}", 3600, "1")
return result
Use a time-based cache or database to track which events have been processed.
When processing speed lags behind arrival rate, backpressure must be handled gracefully.
# Consumer with rate limiting
kafka-console-consumer.sh \
--bootstrap-server kafka.example.com:9092 \
--topic high-volume-topic \
--max.poll.records=100 \
--poll.interval.ms=1000
The --max.poll.records parameter limits how many messages are fetched per poll cycle. This prevents overwhelming downstream systems.
# Producer with buffer settings
kafka-console-producer.sh \
--bootstrap-server kafka.example.com:9092 \
--topic buffered-topic \
--buffer.memory=33554432 \
--retries=3
The --buffer.memory parameter sets how much memory to use for buffering. If this is too small, producers may block waiting for space.
Stream processors must expose metrics for operational visibility:
# Check consumer lag
kafka-consumer-groups.sh \
--bootstrap-server kafka.example.com:9092 \
--describe \
--group my-group
The output shows Lag which indicates how many messages behind the consumer is. If this value grows continuously, it signals a processing problem.
Many stream processors expose metrics via HTTP:
# Example metrics endpoint
curl http://processor.example.com/metrics
Look for metrics like throughput, error rates, and latency. The response format depends on the implementation but typically returns JSON or Prometheus-style text.
Stream processors may silently drop events when they encounter configuration errors:
# Wrong deserializer causes silent failures
kafka-console-consumer.sh \
--bootstrap-server kafka.example.com:9092 \
--topic user-events \
--value-deserializer=org.apache.kafka.common.serialization.IntegerDeserializer
If events are JSON strings but the deserializer expects integers, messages will be skipped without error reporting.
The --enable.auto.commit flag should generally be disabled for production:
# Producer with manual offset control
kafka-console-consumer.sh \
--bootstrap-server kafka.example.com:9092 \
--topic user-events \
--enable.auto.commit=false \
--isolation.level=read_committed
When auto-commit is enabled, a consumer might commit an offset before successfully processing all messages in that batch.
Stream processors can exhaust memory or CPU:
# Monitor consumer resource usage
top -p $(pgrep -f kafka-console-consumer)
Check for high CPU usage or memory leaks in long-running consumers. Large stateful operations may require tuning JVM parameters.
Add more consumers to a consumer group to increase throughput:
# Start multiple consumers
kafka-console-consumer.sh \
--bootstrap-server kafka.example.com:9092 \
--topic user-events \
--group scaling-group \
--threads 4
The --threads parameter increases parallelism within a single consumer process.
Stream processors should be designed for rolling updates:
# Example deployment script
kubectl set image deployment/stream-processor processor=stream-processor:v2.1
Use readiness probes to ensure new instances are fully initialized before accepting traffic. The --wait flag in Kubernetes deployments ensures proper startup sequencing.
Ensure your topics have sufficient partitions for parallel processing:
# Check topic configuration
kafka-topics.sh \
--bootstrap-server kafka.example.com:9092 \
--describe \
--topic user-events
Look for the PartitionCount field. Too few partitions limit horizontal scaling capability.
# Producer with optimal settings
kafka-console-producer.sh \
--bootstrap-server kafka.example.com:9092 \
--topic reliable-topic \
--acks=1 \
--retries=3 \
--batch.size=16384
The --acks=1 setting ensures messages are written to at least one replica before acknowledging. Use --retries=3 for transient failures.
# Consumer with safe settings
kafka-console-consumer.sh \
--bootstrap-server kafka.example.com:9092 \
--topic critical-topic \
--group safety-group \
--enable.auto.commit=false \
--max.poll.interval.ms=300000 \
--session.timeout.ms=10000
The --max.poll.interval.ms parameter controls how long consumers can take between poll operations before being considered dead. Set this to at least 5 minutes for long-running processing.
Stream processing requires careful attention to data flow, error handling, and system boundaries. The key is maintaining consistency while allowing for real-time responsiveness.
This page was rewritten on 16 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.
We use cookies for analytics (Google Analytics) and advertising (Google AdSense) to improve your experience and support free content. Privacy Policy