Apache Flink - Advanced Stream Processing
Advanced stream processing with event time, stateful computations, and exactly-once guarantees
True Stream Processing at Scale
Apache Flink is a distributed stream processing framework designed from the ground up for real-time analytics and event-driven applications. Unlike Spark Streaming's micro-batch approach, Flink processes data as true continuous streams with millisecond latency. It handles event time processing, complex stateful computations, and provides exactly-once consistency guarantees, making it the go-to choice for mission-critical streaming applications like fraud detection, real-time recommendations, and financial trading systems. If your use case demands low latency, sophisticated windowing, and bulletproof correctness, Flink is the answer.
What is Apache Flink?
Apache Flink is a stateful stream processing framework that treats batch processing as a special case of stream processing (finite streams). It was built by the original creators of MapReduce and offers unmatched performance for real-time data pipelines.
Flink Architecture:
┌─────────────────────────────────────────────────────┐
│ Data Sources │
│ (Kafka, Kinesis, Files, Sockets, Custom) │
└───────────────────┬─────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ Flink DataStream API │
│ • Event-time processing │
│ • Windowing (tumbling, sliding, session) │
│ • Stateful operations (with checkpoints) │
│ • Exactly-once semantics │
└───────────────────┬─────────────────────────────────┘
│
┌───────────┴────────────┐
▼ ▼
┌───────────────┐ ┌──────────────┐
│ JobManager │ │ TaskManagers │
│ (Coordinator) │◄──────►│ (Workers) │
│ │ │ │
│ • Scheduling │ │ • Execute │
│ • Checkpoints │ │ tasks │
│ • Recovery │ │ • State │
└───────────────┘ └──────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ Data Sinks │
│ (Kafka, Databases, Files, Dashboards) │
└─────────────────────────────────────────────────────┘Key Capabilities
⚡ True Streaming
Process events one at a time with millisecond latency (not micro-batches)
🕒 Event Time
Process events based on when they occurred, not when they arrive
💾 Stateful
Maintain state across billions of events with fault-tolerant checkpoints
✓ Exactly-Once
Guarantee each event is processed exactly once, even with failures
🪟 Advanced Windowing
Tumbling, sliding, session, and custom windows with late data handling
🔄 Savepoints
Snapshot state for versioning, rollbacks, and application upgrades
Flink vs Spark Streaming: Architecture Comparison
The fundamental difference: Flink processes true continuous streams, while Spark Streaming uses micro-batches. This architectural choice cascades into latency, windowing, and state management.
Spark Streaming: Micro-Batch Processing
Treats streaming as a series of small batch jobs
Spark Streaming Model:
Time: 0s 1s 2s 3s 4s
│ │ │ │ │
Events: ████ ████ ████ ████ ████
Batch 1 Batch 2 Batch 3 Batch 4 Batch 5
│ │ │ │ │
└──────────┴──────────┴──────────┴──────────┘
Process each micro-batch using Spark RDDs
Latency: ~500ms - 2 seconds (limited by batch interval)
Model: Discrete time intervals, not continuous
State: Managed at batch boundariesExample: Spark Structured Streaming
from pyspark.sql import SparkSession
from pyspark.sql.functions import window, col
spark = SparkSession.builder.appName("SparkStreaming").getOrCreate()
# Read stream from Kafka (micro-batches)
df = spark.readStream \
.format("kafka") \
.option("kafka.bootstrap.servers", "localhost:9092") \
.option("subscribe", "transactions") \
.load()
# Parse JSON
transactions = df.selectExpr("CAST(value AS STRING)") \
.selectExpr("from_json(value, schema) as data") \
.select("data.*")
# Windowed aggregation (processing time by default)
windowed_totals = transactions \
.groupBy(
window("timestamp", "1 minute"), # 1-minute windows
"user_id"
) \
.agg({"amount": "sum"})
# Write to console (triggers micro-batch processing)
query = windowed_totals.writeStream \
.outputMode("update") \
.format("console") \
.trigger(processingTime="5 seconds") # Process every 5 seconds \
.start()
query.awaitTermination()Processes transactions in 5-second micro-batches
Latency: 5+ seconds (limited by trigger interval)
Windows aligned to processing time
Apache Flink: True Stream Processing
Processes events continuously, one at a time
Flink Model:
Time: 0s 1s 2s 3s 4s
│ │ │ │ │
Events: █─█──█───█─█─█──█───█──█─█────█──█───█─█──
Each event processed immediately as it arrives
Latency: ~1ms - 100ms (sub-second)
Model: Continuous, record-at-a-time processing
State: Maintained continuously with checkpointsExample: Flink DataStream API (PyFlink)
from pyflink.datastream import StreamExecutionEnvironment
from pyflink.datastream.window import TumblingEventTimeWindows
from pyflink.common import Time, WatermarkStrategy
from pyflink.common.serialization import SimpleStringSchema
from pyflink.datastream.connectors.kafka import KafkaSource
env = StreamExecutionEnvironment.get_execution_environment()
env.set_parallelism(4)
# Read stream from Kafka (true streaming, not batches)
kafka_source = KafkaSource.builder() \
.set_bootstrap_servers("localhost:9092") \
.set_topics("transactions") \
.set_value_only_deserializer(SimpleStringSchema()) \
.build()
# Define watermark strategy for event time
watermark_strategy = WatermarkStrategy \
.for_bounded_out_of_orderness(Time.seconds(5)) \ # 5 sec max lateness
.with_timestamp_assigner(lambda event: event['timestamp'])
# Create stream with event time
stream = env.from_source(
kafka_source,
watermark_strategy,
"kafka-source"
)
# Parse JSON and extract fields
transactions = stream.map(lambda x: json.loads(x))
# Windowed aggregation using EVENT TIME (not processing time!)
windowed_totals = transactions \
.key_by(lambda x: x['user_id']) \
.window(TumblingEventTimeWindows.of(Time.minutes(1))) \ # 1-min event time windows
.reduce(lambda a, b: {
'user_id': a['user_id'],
'total': a['total'] + b['amount']
})
# Sink to console
windowed_totals.print()
# Execute (streaming job runs continuously)
env.execute("Flink Streaming")Processes each transaction immediately (millisecond latency)
Windows based on event timestamps, not arrival time
Handles out-of-order events with watermarks
State maintained continuously with checkpoints
Feature Comparison
| Feature | Spark Streaming | Apache Flink |
|---|---|---|
| Processing Model | Micro-batches (discrete intervals) | True streaming (continuous) |
| Latency | 500ms - 2 seconds | 1ms - 100ms |
| Event Time Support | Limited (added in Structured Streaming) | Native, first-class support |
| Windowing | Basic (tumbling, sliding) | Advanced (tumbling, sliding, session, custom) |
| State Management | At batch boundaries | Continuous with checkpoints |
| Exactly-Once | Yes (with structured streaming) | Yes (built-in, more mature) |
| Backpressure | Limited | Excellent (credit-based flow control) |
| Use Case | Near real-time analytics, existing Spark ecosystem | Low-latency, complex event processing, mission-critical |
| Batch + Stream | Batch first, streaming added | Streaming first, batch is special case |
| Community | Larger (Spark ecosystem) | Specialized (streaming experts) |
Spark Streaming: When you already use Spark, need batch + stream, or latency >1 second is acceptable
Flink: When you need sub-second latency, complex event time processing, or sophisticated state management
Event Time Processing & Windowing
Event time processing is Flink's superpower. Instead of processing events based on when they arrive (processing time), Flink uses timestamps embedded in the events themselves, enabling correct results even with out-of-order and late-arriving data.
Processing Time vs Event Time
PROCESSING TIME (arrival time):
Events arrive: E1(10:00) → E2(10:01) → E3(10:02)
Processed at: 10:05 10:06 10:07
Window: [10:05-10:06], [10:06-10:07], [10:07-10:08]
Problem: E1 happened at 10:00 but gets windowed at 10:05!
Results wrong if events delayed or out-of-order
EVENT TIME (when event actually occurred):
Events arrive: E1(10:00) → E3(10:02) → E2(10:01) ← Out of order!
Event timestamp: 10:00 10:02 10:01
Window: [10:00-10:01], [10:01-10:02], [10:02-10:03]
Correct: Each event placed in window based on its actual timestamp
Results accurate regardless of arrival order or delaysWatermarks: Tracking Progress in Event Time
Watermarks tell Flink: "I've seen all events up to time T". This allows Flink to know when a window is complete and trigger computations, even with out-of-order data.
Watermark Mechanics:
Time: 10:00 10:01 10:02 10:03 10:04 10:05
│ │ │ │ │ │
Events: E1 E3 E2 E5 E4 │
ts:00 ts:02 ts:01 ts:04 ts:03 │
│
Watermark: 00 01 02 03 04 │
│ │ └─Window[00-01] closes │
│ │ (E1, E2 included) │
│ │ │
│ └─Window[01-02] closes │
│ (E2, E3 included) │
│ │
└─E1 processed immediately │
Watermark = max(event_time) - allowed_lateness
= 10:02 - 1 min = 10:01
Window [10:00-10:01] closes when watermark reaches 10:01Example: Configuring Watermarks
from pyflink.common import WatermarkStrategy, Time
# Strategy 1: Bounded out-of-orderness
# Assumes events can be up to 5 seconds late
watermark_strategy = WatermarkStrategy \
.for_bounded_out_of_orderness(Time.seconds(5)) \
.with_timestamp_assigner(lambda event: event['timestamp'])
# Strategy 2: Monotonous timestamps
# Assumes events always arrive in order (no lateness)
watermark_strategy = WatermarkStrategy \
.for_monotonous_timestamps() \
.with_timestamp_assigner(lambda event: event['timestamp'])
# Strategy 3: Custom watermark generator
class CustomWatermarkGenerator(WatermarkGenerator):
def __init__(self):
self.max_timestamp = 0
def on_event(self, event, timestamp):
# Track maximum timestamp seen
self.max_timestamp = max(self.max_timestamp, timestamp)
def on_periodic_emit(self):
# Emit watermark: max_timestamp - 10 seconds
return Watermark(self.max_timestamp - 10000)
# Apply watermark to stream
stream = env.from_source(kafka_source, watermark_strategy, "source")Watermarks allow Flink to handle late events (up to 5 seconds)
Windows close when watermark passes window end time
Balance: larger lateness = more complete results, higher latency
Window Types
1. Tumbling Windows
Fixed-size, non-overlapping windows. Each event belongs to exactly one window.
Tumbling Window (5 seconds):
Time: 0 5 10 15 20 25 30
│────│────│────│────│────│────│
[W1 ][W2 ][W3 ][W4 ][W5 ][W6 ]
Events fall into ONE window only
Use case: Non-overlapping time-based aggregationsfrom pyflink.datastream.window import TumblingEventTimeWindows
from pyflink.common import Time
# Count events per 1-minute tumbling window
windowed_counts = stream \
.key_by(lambda x: x['user_id']) \
.window(TumblingEventTimeWindows.of(Time.minutes(1))) \
.reduce(lambda a, b: {
'user_id': a['user_id'],
'count': a.get('count', 1) + b.get('count', 1)
})2. Sliding Windows
Fixed-size windows that slide by a specified interval. Events can belong to multiple windows.
Sliding Window (size=10s, slide=5s):
Time: 0 5 10 15 20 25 30
│────│────│────│────│────│────│
[──W1────]
[──W2────]
[──W3────]
[──W4────]
[──W5────]
Events fall into MULTIPLE overlapping windows
Use case: Moving averages, trend detectionfrom pyflink.datastream.window import SlidingEventTimeWindows
# Moving average: 5-min window, sliding every 1 minute
moving_avg = stream \
.key_by(lambda x: x['sensor_id']) \
.window(SlidingEventTimeWindows.of(
Time.minutes(5), # Window size
Time.minutes(1) # Slide interval
)) \
.reduce(lambda a, b: {
'sensor_id': a['sensor_id'],
'avg_temp': (a['avg_temp'] + b['temperature']) / 2
})3. Session Windows
Dynamic windows based on event gaps. A session closes after a specified inactivity period.
Session Window (gap=5 min):
Events: E1 E2 E3 E4 E5 E6
Time: :00 :02 :04 :15 :17 :30
│───────────│ │─────│ │
[ Session1 ] [Sess2] [S3]
Sessions defined by activity bursts
Use case: User sessions, clickstream analysisfrom pyflink.datastream.window import EventTimeSessionWindows
# User sessions: close after 30 minutes of inactivity
user_sessions = stream \
.key_by(lambda x: x['user_id']) \
.window(EventTimeSessionWindows.with_gap(Time.minutes(30))) \
.reduce(lambda a, b: {
'user_id': a['user_id'],
'pages_viewed': a.get('pages_viewed', 1) + 1,
'session_duration': b['timestamp'] - a['start_time']
})Handling Late Data
What happens when events arrive after their window has closed? Flink provides side outputs for late data.
from pyflink.datastream.window import TumblingEventTimeWindows
from pyflink.common import Time
from pyflink.datastream import OutputTag
# Define late data tag
late_data_tag = OutputTag("late-data", Types.STRING())
# Configure window with allowed lateness
windowed_stream = stream \
.key_by(lambda x: x['user_id']) \
.window(TumblingEventTimeWindows.of(Time.minutes(1))) \
.allowed_lateness(Time.minutes(5)) \ # Allow 5 min late data
.side_output_late_data(late_data_tag) \ # Send late data to side output
.reduce(lambda a, b: {
'user_id': a['user_id'],
'total': a['total'] + b['amount']
})
# Get late events from side output
late_events = windowed_stream.get_side_output(late_data_tag)
# Process late events separately (log, store for reprocessing, etc.)
late_events.print("LATE EVENT")Events up to 5 minutes late are included in window
Events more than 5 minutes late go to side output
Prevents indefinite waiting while handling stragglers
Stateful Computations at Scale
Flink's state management allows operations to remember information across events. This enables complex patterns like aggregations, joins, machine learning models, and fraud detection that need to track state for millions of keys (users, devices, sessions).
Types of State
Keyed State
State scoped to a specific key (user, device, session)
• ValueState: Single value per key• ListState: List of values per key
• MapState: Key-value map per key
• ReducingState: Aggregated value per key
Operator State
State scoped to an operator instance (not keyed)
• ListState: List accessible by operator• UnionListState: Merged on restore
• BroadcastState: Shared across all parallel instances
Example: Fraud Detection with Keyed State
Track transaction amounts per user to detect suspicious patterns.
from pyflink.datastream import StreamExecutionEnvironment, KeyedProcessFunction
from pyflink.common.typeinfo import Types
from pyflink.datastream.state import ValueStateDescriptor
class FraudDetector(KeyedProcessFunction):
"""Detect fraud: alert if 2 large transactions within 1 minute"""
def open(self, runtime_context):
# Initialize state: last large transaction timestamp
self.last_large_tx_state = runtime_context.get_state(
ValueStateDescriptor("last-large-tx", Types.LONG())
)
def process_element(self, transaction, ctx):
# Check if transaction is large (> $1000)
if transaction['amount'] > 1000:
# Get last large transaction time
last_large_tx_time = self.last_large_tx_state.value()
if last_large_tx_time is not None:
# Check if within 1 minute
time_diff = transaction['timestamp'] - last_large_tx_time
if time_diff < 60000: # 60 seconds in milliseconds
# FRAUD DETECTED!
yield {
'user_id': transaction['user_id'],
'alert': 'FRAUD',
'reason': f'Two large transactions within {time_diff/1000}s',
'amount1': last_large_tx_time,
'amount2': transaction['amount']
}
# Update state with current transaction time
self.last_large_tx_state.update(transaction['timestamp'])
# Apply fraud detector
env = StreamExecutionEnvironment.get_execution_environment()
transactions = env.from_collection([
{'user_id': 'U123', 'amount': 1500, 'timestamp': 1000},
{'user_id': 'U123', 'amount': 2000, 'timestamp': 30000}, # 29s later - FRAUD!
{'user_id': 'U456', 'amount': 500, 'timestamp': 5000},
{'user_id': 'U123', 'amount': 3000, 'timestamp': 120000}, # >1min later - OK
])
alerts = transactions \
.key_by(lambda x: x['user_id']) \
.process(FraudDetector())
alerts.print()
env.execute("Fraud Detection")User U123: Alert triggered for 2nd large transaction (29 seconds apart)
State maintained per user_id across all events
Scales to millions of users with efficient state backend
State Backends: Where State Lives
Flink stores state in backends that balance performance and durability.
| Backend | Storage | Performance | Use Case |
|---|---|---|---|
| MemoryStateBackend | Heap memory | Fastest | Testing, small state |
| FsStateBackend | Heap + file system checkpoints | Fast | Medium state (< few GB) |
| RocksDBStateBackend | RocksDB (disk) + distributed storage | Slower, scalable | Large state (TBs), production |
from pyflink.datastream import StreamExecutionEnvironment
env = StreamExecutionEnvironment.get_execution_environment()
# Configure RocksDB state backend (for large state)
env.set_state_backend(
"rocksdb",
checkpoint_path="s3://my-bucket/checkpoints"
)
# Enable checkpointing every 60 seconds
env.enable_checkpointing(60000) # millisecondsCheckpoints vs Savepoints
Checkpoints
Purpose: Automatic fault tolerance
- Triggered automatically (e.g., every 60s)
- Lightweight snapshots
- Used for failure recovery
- Deleted after job stops
- Not portable across Flink versions
Savepoints
Purpose: Manual operational snapshots
- Triggered manually by operator
- Full state snapshot
- Used for upgrades, rollbacks, A/B testing
- Persist indefinitely
- Portable (version compatible)
# Create a savepoint manually (via CLI) $ flink savepoint <job-id> s3://my-bucket/savepoints # Stop job with savepoint $ flink stop --savepointPath s3://my-bucket/savepoints <job-id> # Restore from savepoint (e.g., after code upgrade) $ flink run -s s3://my-bucket/savepoints/savepoint-123 my-job.jar
Exactly-Once Semantics
Flink guarantees each event is processed exactly once, even with failures. This is critical for financial transactions, billing, and any use case where duplicates or data loss is unacceptable.
Processing Guarantees Explained
At-Most-Once
Events may be lost but never duplicated
Example: Logs, metrics (data loss OK)Problem: Missing data
At-Least-Once
Events never lost but may be duplicated
Example: Alert systems (duplicates OK)Problem: Duplicates inflate counts
Exactly-Once
Each event processed once, no loss, no duplicates
Example: Financial transactions, billingBenefit: Guaranteed correctness
How Exactly-Once Works in Flink
Flink achieves exactly-once through a combination of distributed snapshots (checkpoints) and transactional sinks.
Flink's Exactly-Once Mechanism: 1. Distributed Snapshots (Chandy-Lamport Algorithm): ┌─────────────────────────────────────────────┐ │ Checkpoint Barrier flows through pipeline │ │ │ │ Source ─barrier→ Op1 ─barrier→ Op2 ─Sink │ │ │ │ │ │ │ └─ state ───────┴─ state ┴──── state │ │ │ │ When barrier reaches operator: │ │ • Snapshot state │ │ • Forward barrier │ └─────────────────────────────────────────────┘ 2. Two-Phase Commit for Sinks: ┌─────────────────────────────────────────────┐ │ Checkpoint N started: │ │ Phase 1 (Pre-commit): Stage writes │ │ │ │ Checkpoint N completes: │ │ Phase 2 (Commit): Atomically commit │ └─────────────────────────────────────────────┘ 3. On Failure: • Restore state from last successful checkpoint • Replay events from checkpoint onwards • No duplicates or data loss
Enabling Exactly-Once
from pyflink.datastream import StreamExecutionEnvironment, CheckpointingMode
env = StreamExecutionEnvironment.get_execution_environment()
# Enable checkpointing with exactly-once mode
env.enable_checkpointing(60000, CheckpointingMode.EXACTLY_ONCE)
# Configure checkpoint settings
checkpoint_config = env.get_checkpoint_config()
# Checkpoint timeout (max time for checkpoint to complete)
checkpoint_config.set_checkpoint_timeout(300000) # 5 minutes
# Minimum time between checkpoints (avoid too frequent)
checkpoint_config.set_min_pause_between_checkpoints(30000) # 30 seconds
# Max concurrent checkpoints
checkpoint_config.set_max_concurrent_checkpoints(1)
# Retain checkpoints on job cancellation (for recovery)
checkpoint_config.enable_externalized_checkpoints(
ExternalizedCheckpointCleanup.RETAIN_ON_CANCELLATION
)
# Configure Kafka sink with exactly-once (requires Kafka 0.11+)
from pyflink.datastream.connectors.kafka import KafkaSink, KafkaRecordSerializationSchema
kafka_sink = KafkaSink.builder() \
.set_bootstrap_servers("localhost:9092") \
.set_record_serializer(
KafkaRecordSerializationSchema.builder()
.set_topic("output-topic")
.set_value_serialization_schema(SimpleStringSchema())
.build()
) \
.set_delivery_guarantee(DeliveryGuarantee.EXACTLY_ONCE) \ # Exactly-once!
.set_transactional_id_prefix("flink-txn-") \
.build()
# Write to Kafka with exactly-once guarantees
stream.sink_to(kafka_sink)
env.execute("Exactly-Once Pipeline")Checkpoints every 60 seconds with exactly-once mode
Kafka sink uses two-phase commit for exactly-once delivery
On failure, job recovers from last checkpoint with no duplicates or data loss
Requirements for End-to-End Exactly-Once
✅ Sources
- Must be replayable (Kafka offsets, file positions)
- Flink resets source to checkpoint position on recovery
- Supported: Kafka, Kinesis, file systems
✅ Sinks
- Must support transactions or idempotent writes
- Two-phase commit (Kafka, databases with ACID)
- Supported: Kafka, Postgres, MySQL, file systems
Complete Example: Real-Time Analytics Pipeline
Let's build a complete Flink pipeline: ingest clickstream data, detect user sessions, calculate metrics, and sink to Kafka with exactly-once guarantees.
from pyflink.datastream import StreamExecutionEnvironment, KeyedProcessFunction, CheckpointingMode
from pyflink.datastream.window import EventTimeSessionWindows, TumblingEventTimeWindows
from pyflink.common import WatermarkStrategy, Time, Types
from pyflink.common.serialization import SimpleStringSchema
from pyflink.datastream.connectors.kafka import KafkaSource, KafkaSink, KafkaRecordSerializationSchema, DeliveryGuarantee
from pyflink.datastream.state import ValueStateDescriptor
import json
env = StreamExecutionEnvironment.get_execution_environment()
env.set_parallelism(4)
# Enable checkpointing (exactly-once)
env.enable_checkpointing(60000, CheckpointingMode.EXACTLY_ONCE)
# ============ 1. INGEST FROM KAFKA ============
kafka_source = KafkaSource.builder() \
.set_bootstrap_servers("localhost:9092") \
.set_topics("clickstream") \
.set_group_id("flink-analytics") \
.set_value_only_deserializer(SimpleStringSchema()) \
.build()
# Watermark: 5 seconds max lateness
watermark_strategy = WatermarkStrategy \
.for_bounded_out_of_orderness(Time.seconds(5)) \
.with_timestamp_assigner(lambda event: json.loads(event)['timestamp'])
clickstream = env.from_source(kafka_source, watermark_strategy, "clickstream-source")
# ============ 2. PARSE & ENRICH ============
def parse_click(event_json):
event = json.loads(event_json)
return {
'user_id': event['user_id'],
'page': event['page'],
'timestamp': event['timestamp'],
'session_id': None # Will be set by session window
}
clicks = clickstream.map(parse_click)
# ============ 3. SESSION DETECTION ============
# Group into sessions: 30 min inactivity timeout
user_sessions = clicks \
.key_by(lambda x: x['user_id']) \
.window(EventTimeSessionWindows.with_gap(Time.minutes(30))) \
.reduce(lambda a, b: {
'user_id': a['user_id'],
'pages_viewed': a.get('pages_viewed', 1) + 1,
'start_time': min(a.get('start_time', a['timestamp']), b['timestamp']),
'end_time': max(a.get('end_time', a['timestamp']), b['timestamp']),
'duration': b['timestamp'] - a.get('start_time', a['timestamp'])
})
# ============ 4. ANOMALY DETECTION ============
class AnomalyDetector(KeyedProcessFunction):
"""Alert if session duration > 2 hours (unusual)"""
def open(self, runtime_context):
self.avg_duration_state = runtime_context.get_state(
ValueStateDescriptor("avg-duration", Types.FLOAT())
)
def process_element(self, session, ctx):
duration_minutes = session['duration'] / 60000
# Get running average
avg_duration = self.avg_duration_state.value()
if avg_duration is None:
avg_duration = duration_minutes
else:
# Exponential moving average
avg_duration = 0.9 * avg_duration + 0.1 * duration_minutes
# Update state
self.avg_duration_state.update(avg_duration)
# Detect anomaly
if duration_minutes > 120: # > 2 hours
yield {
'type': 'ANOMALY',
'user_id': session['user_id'],
'duration_minutes': duration_minutes,
'avg_duration_minutes': avg_duration,
'deviation': duration_minutes / avg_duration if avg_duration > 0 else 0
}
else:
yield {
'type': 'NORMAL',
'user_id': session['user_id'],
'duration_minutes': duration_minutes,
'pages_viewed': session['pages_viewed']
}
alerts = user_sessions \
.key_by(lambda x: x['user_id']) \
.process(AnomalyDetector())
# ============ 5. AGGREGATED METRICS ============
# Count sessions per 5-minute tumbling window
session_counts = user_sessions \
.window(TumblingEventTimeWindows.of(Time.minutes(5))) \
.reduce(lambda a, b: {
'window': 'count',
'total_sessions': a.get('total_sessions', 1) + 1,
'total_pages': a.get('total_pages', a['pages_viewed']) + b['pages_viewed']
})
# ============ 6. SINK TO KAFKA (EXACTLY-ONCE) ============
kafka_sink = KafkaSink.builder() \
.set_bootstrap_servers("localhost:9092") \
.set_record_serializer(
KafkaRecordSerializationSchema.builder()
.set_topic("analytics-output")
.set_value_serialization_schema(
lambda x: json.dumps(x).encode('utf-8')
)
.build()
) \
.set_delivery_guarantee(DeliveryGuarantee.EXACTLY_ONCE) \
.set_transactional_id_prefix("flink-analytics-") \
.build()
# Write both alerts and metrics
alerts.sink_to(kafka_sink)
session_counts.sink_to(kafka_sink)
# ============ 7. EXECUTE ============
env.execute("Real-Time Clickstream Analytics")1. Ingest clickstream from Kafka with event time
2. Detect user sessions (30-min inactivity timeout)
3. Detect anomalies (sessions >2 hours) using stateful computation
4. Calculate aggregated metrics (sessions per 5-min window)
5. Write to Kafka with exactly-once guarantees
6. Handles out-of-order events, failures, and provides sub-second latency
Bonus: Real-Time E-Commerce Analytics with PyFlink
Every concept from this lesson: event time, watermarks, tumbling and sliding windows, RocksDB state backend, and exactly-once checkpointing, is demonstrated in a working project using PyFlink 2.2, Kafka (KRaft), and PostgreSQL. A single Flink job runs three concurrent aggregations as one StatementSet, all reading from the same Kafka source, and writes results to PostgreSQL via JDBC upsert. A Streamlit dashboard reads directly from Kafka for a live view without touching the database.
The three aggregations:
- revenue_by_category: 1-minute tumbling window: total revenue and order count per product category per minute
- product_performance: 5-minute HOP window with 1-minute slide: rolling revenue per product, each event appears in 5 consecutive windows for a smooth trend line
- user_order_velocity: 5-minute tumbling window with a fraud flag: per-user order count, spend, and max order amount;
is_suspicious = TRUEwhenorder_count > 10ormax_order_amount > $1,000in any 5-minute window
# Prerequisites: Docker >= 24, Python 3.12, make git clone https://gitlab.com/bytecode-solutions/examples/flink-streaming-processing cd flink-streaming-processing python3.12 -m venv .venv && source .venv/bin/activate make install # pins setuptools=70 to work around apache-beam/pkg_resources issue make build # custom Flink image with connector JARs baked in make up # Flink Web UI → :8081 | Kafka UI → :8080 make submit # submit the Flink job (detached, runs on cluster) # Produce order events from your laptop make produce # 10 msg/s, 2% fraud rate make produce-fraud # 30 msg/s, 100% fraud – triggers fraud window immediately # Live Streamlit dashboard (reads from Kafka, no DB required) make dashboard # → http://localhost:8501 # Query results in PostgreSQL make psql # analytics=# SELECT * FROM revenue_by_category ORDER BY window_end DESC LIMIT 10; # analytics=# SELECT * FROM user_order_velocity WHERE is_suspicious = TRUE; # Run tests and linters make test # pytest -v tests/ (validates DDL + event shapes without a live cluster) make lint # ruff + mypy
Key Takeaways
- Flink processes true continuous streams (not micro-batches)
- Event time enables correct results with out-of-order data
- Watermarks track progress in event time and trigger windows
- Windowing supports tumbling, sliding, session, and custom patterns
- Stateful computations scale to millions of keys with checkpoints
- Exactly-once guarantees correctness for mission-critical use cases
- RocksDB backend enables TBs of state
- Savepoints allow zero-downtime upgrades and rollbacks