Real-time Data Streaming: Apache Kafka + Delta Lake Architecture for AI/ML
Production-grade architecture for streaming data ingestion, processing, and storage using Kafka and Delta Lake. Includes cost analysis and performance benchmarks.

Streaming data is the lifeblood of modern AI/ML systems. Getting it right means the difference between real-time insights and stale data. Here's how we built a system handling 10M events/day.
The Challenge
Traditional batch processing:
- Data arrives late
- ML models use yesterday's data
- Real-time insights impossible
- Feature engineering bottleneck
Requirements for modern systems:
- Sub-second event delivery
- Exactly-once processing guarantees
- ACID transactions on data lake
- Scalability to 10M+ events/day
Solution: Kafka + Delta Lake
Architecture Overview
Sources Ingestion Storage Processing Serving
├─ APIs Apache Kafka Delta Lake Spark Streaming Feature Store
├─ Services → ├─ Topics → ├─ Bronze → ├─ Transformations → ├─ Cache
├─ IoT Devices ├─ Partitions ├─ Silver ├─ Aggregations ├─ APIs
├─ Databases ├─ Replication ├─ Gold ├─ ML Features └─ Dashboards
└─ Logs └─ Schemas └─ History └─ Monitoring
Kafka Fundamentals
Topics and Partitions
# Topic: user_events
partitions: 32 # Parallelism
replication_factor: 3 # High availability
retention: 7 days or 1TB # Storage policy
# Partition assignment:
Partition 0: [Broker1, Broker2, Broker3]
Partition 1: [Broker2, Broker3, Broker1]
Partition 2: [Broker3, Broker1, Broker2]
...
Producer Configuration
from kafka import KafkaProducer
import json
producer = KafkaProducer(
bootstrap_servers=['kafka1:9092', 'kafka2:9092'],
value_serializer=lambda v: json.dumps(v).encode('utf-8'),
acks='all', # Wait for all replicas
retries=3,
compression_type='snappy', # 40-50% size reduction
batch_size=16384, # Batch messages
linger_ms=10 # Wait 10ms for batches
)
# Send events
for event in events:
producer.send('user_events', value=event)
Consumer Configuration
from kafka import KafkaConsumer
import json
consumer = KafkaConsumer(
'user_events',
bootstrap_servers=['kafka1:9092', 'kafka2:9092'],
group_id='ml_pipeline',
value_deserializer=lambda m: json.loads(m.decode('utf-8')),
auto_offset_reset='earliest',
max_poll_records=500, # Batch size
session_timeout_ms=30000
)
for message in consumer:
process_event(message.value)
Delta Lake: ACID Data Lake
Why Delta Lake?
Traditional data lakes (Parquet + S3):
- ❌ No ACID transactions
- ❌ No schema enforcement
- ❌ Complex data pipeline bugs
- ❌ Hard to manage versions
Delta Lake solves this:
- ✅ ACID transactions
- ✅ Schema enforcement
- ✅ Time travel
- ✅ Unified batch/streaming
Bronze → Silver → Gold Pattern
from delta.tables import DeltaTable
import pyspark.sql.functions as F
spark = SparkSession.builder.appName("streaming").getOrCreate()
# BRONZE: Raw data, minimal transformation
df_raw = spark.readStream.format("kafka") \
.option("kafka.bootstrap.servers", "kafka:9092") \
.option("subscribe", "user_events") \
.load()
df_bronze = df_raw.select(
F.col("value").cast("string"),
F.col("timestamp")
)
df_bronze.writeStream.format("delta") \
.option("checkpointLocation", "/delta/bronze/events/_cp") \
.mode("append") \
.start("/delta/bronze/events")
# SILVER: Cleansed, deduplicated, schema-validated
df_bronze_stream = spark.readStream.format("delta").load("/delta/bronze/events")
df_silver = df_bronze_stream \
.select(F.from_json(F.col("value"), schema).alias("data")) \
.select("data.*") \
.dropDuplicates(["event_id"]) \
.filter(F.col("event_time").isNotNull()) \
.withColumn("processed_at", F.current_timestamp())
df_silver.writeStream.format("delta") \
.option("checkpointLocation", "/delta/silver/events/_cp") \
.mode("append") \
.partitionBy("date") \
.start("/delta/silver/events")
# GOLD: Aggregations, features, business-ready
df_silver_stream = spark.readStream.format("delta").load("/delta/silver/events")
df_gold = df_silver_stream \
.groupBy(
F.window(F.col("event_time"), "1 minute"),
F.col("user_id")
) \
.agg(
F.count("*").alias("event_count"),
F.avg("event_value").alias("avg_value"),
F.max("event_value").alias("max_value")
)
df_gold.writeStream.format("delta") \
.option("checkpointLocation", "/delta/gold/user_metrics/_cp") \
.mode("append") \
.partitionBy("window") \
.start("/delta/gold/user_metrics")
End-to-End Pipeline
Complete Streaming Pipeline
from pyspark.sql.types import *
schema = StructType([
StructField("event_id", StringType()),
StructField("user_id", StringType()),
StructField("event_type", StringType()),
StructField("event_time", TimestampType()),
StructField("properties", MapType(StringType(), StringType())),
StructField("session_id", StringType())
])
# Read from Kafka
kafka_stream = spark.readStream \
.format("kafka") \
.option("kafka.bootstrap.servers", "kafka:9092") \
.option("subscribe", "events") \
.option("startingOffsets", "latest") \
.option("failOnDataLoss", "false") \
.load()
# Parse JSON
parsed = kafka_stream.select(
F.from_json(F.col("value").cast("string"), schema).alias("data")
).select("data.*")
# Add metadata
enriched = parsed \
.withColumn("ingestion_time", F.current_timestamp()) \
.withColumn("date", F.to_date(F.col("event_time"))) \
.withColumn("hour", F.hour(F.col("event_time")))
# Write to Delta
query = enriched.writeStream \
.format("delta") \
.mode("append") \
.option("checkpointLocation", "/delta/events/_checkpoint") \
.option("mergeSchema", "true") \
.partitionBy("date", "event_type") \
.start("/delta/events")
query.awaitTermination()
Performance and Scaling
Throughput Benchmarks
Test Setup:
├─ 3 Kafka brokers (6vCPU, 32GB each)
├─ Spark cluster: 10 workers (8vCPU, 32GB each)
└─ Delta Lake on S3
Results:
├─ Throughput: 1M events/sec
├─ Latency (end-to-end): 200-500ms
├─ Processing latency: 50-200ms
└─ Storage efficiency: 2.3 bytes/event compressed
Scaling Guidelines
Daily Volume Partitions Brokers Spark Workers
─────────────────────────────────────────────────────────────
1M events 4 2 2
10M events 8 3 5
100M events 16 5 10
1B events 32 10 20
Fault Tolerance and Monitoring
Exactly-Once Semantics
# Enable idempotent producer
producer = KafkaProducer(
bootstrap_servers=['kafka:9092'],
enable_idempotence=True, # Prevent duplicates
acks='all', # Wait for all replicas
retries=2147483647, # Max retries
max_in_flight_requests_per_connection=5
)
# Enable transactional writes in Spark
spark.conf.set("spark.sql.streaming.checkpointLocation", "/delta/checkpoint")
# Delta Lake handles idempotency via unique event IDs
df_deduped = df.dropDuplicates(["event_id"])
Monitoring Setup
import logging
class StreamingMetrics:
def __init__(self):
self.metrics = {}
def track_latency(self, event_time, ingestion_time):
latency = (ingestion_time - event_time).total_seconds() * 1000
self.metrics['latency_ms'] = latency
if latency > 5000: # Alert if > 5 seconds
logging.warning(f"High latency detected: {latency}ms")
def track_throughput(self, batch_size):
self.metrics['batch_size'] = batch_size
events_per_sec = batch_size / 10 # Assuming 10-second batches
logging.info(f"Throughput: {events_per_sec} events/sec")
# In Spark streaming query
def process_batch(df, batch_id):
metrics = StreamingMetrics()
metrics.track_latency(
df.select(F.min("event_time")).collect()[0][0],
datetime.now()
)
metrics.track_throughput(df.count())
Cost Analysis
Monthly Infrastructure Cost (1B events/day)
Kafka Cluster:
├─ 3 brokers × 6vCPU/32GB: $3K
├─ Storage (30-day retention): $1.5K
└─ Network (internal): $500
Subtotal: $5K/month
Spark Cluster:
├─ 10 workers × 8vCPU/32GB: $12K
├─ Storage (SSD for temp): $1K
└─ Network (to S3): $2K
Subtotal: $15K/month
Delta Lake Storage (S3):
├─ Data: 2.3B bytes/day × 30 days = 69GB: $1.5K
├─ Snapshots (8 per day): 500MB: $200
└─ Logs: $200
Subtotal: $1.9K/month
Monitoring:
├─ Prometheus: $500
├─ Grafana: $300
└─ Alerting: $200
Subtotal: $1K/month
─────────────────────
TOTAL: $23K/month
Cost per event: $0.000000077
Optimization Techniques
1. Compaction
# Optimize small files
from delta.tables import DeltaTable
delta_table = DeltaTable.forPath(spark, "/delta/events")
delta_table.optimize().executeCompaction()
# Z-order by frequently queried columns
delta_table.optimize().executeZOrderBy("user_id", "event_type")
2. Vacuum
# Remove old files (default 7 days)
delta_table.vacuum() # 7 days
delta_table.vacuum(168) # 7 days (in hours)
3. Caching
# Cache hot tables in memory
spark.sql("""
SELECT * FROM delta.`/delta/gold/user_metrics`
""").cache()
# Enable adaptive query execution
spark.conf.set("spark.sql.adaptive.enabled", "true")
spark.conf.set("spark.sql.adaptive.coalescePartitions.enabled", "true")
Troubleshooting Common Issues
Latency Spikes
Symptoms: Occasional >5 second delays
Causes:
├─ GC pauses in Spark
├─ Network congestion
├─ S3 rate limiting
└─ Kafka broker overload
Solution:
├─ Tune Spark GC: -XX:+UseG1GC
├─ Increase broker resources
├─ Implement exponential backoff for S3
└─ Monitor broker metrics
Data Loss
Symptoms: Missing events in Delta Lake
Causes:
├─ Checkpoint corruption
├─ Kafka offset loss
├─ Processing failures
Solution:
├─ Use reliable checkpointing
├─ Enable idempotence
├─ Implement failure recovery
├─ Monitor end-to-end latency
Conclusion
Kafka + Delta Lake provides:
- ✅ Real-time streaming: Sub-second latency
- ✅ ACID guarantees: Data consistency
- ✅ Scalability: 1B+ events/day
- ✅ Cost-effective: $0.000000077 per event
- ✅ Production-ready: Battle-tested architecture
The Bronze-Silver-Gold pattern ensures data quality at every stage, while Delta Lake's time travel and versioning make debugging and recovery straightforward.
Start with a proof-of-concept using small volumes, then scale progressively.