JSONL for Data Streaming
Stream millions of events per second with JSONL - the format built for real-time data pipelines, Kafka, and event-driven architectures
Why JSONL for Streaming?
Perfect for Streaming
JSONL's line-based format makes it ideal for streaming data. Each line is a complete event that can be processed independently, allowing for true streaming without waiting for entire payloads to arrive.
- Process events as they arrive
- No buffering required
- Minimal latency
- Fault-tolerant streaming
- Easy to replay and reprocess
Streaming Platforms
Major streaming platforms use JSONL for event serialization:
- Apache Kafka: JSONL event format
- AWS Kinesis: JSON records per partition
- Apache Pulsar: Schema-on-read with JSON
- RabbitMQ: Message payload format
- Redis Streams: Field-value pair serialization
Apache Kafka with JSONL
Kafka Producer (Python)
Produce JSON events to Kafka topics for real-time processing. Use confluent-kafka-python, the officially supported client wrapping the C library librdkafka - it is the maintained option and delivers far higher throughput than a pure-Python client.
Library choice: older tutorials almost always reach for kafka-python. It is effectively unmaintained and should not be chosen for new work. Pick confluent-kafka for production services and aiokafka when you need a native asyncio client.
from confluent_kafka import Producer
import json
import uuid
from datetime import datetime, timezone
# Create producer (librdkafka handles batching, retries and compression)
producer = Producer({
'bootstrap.servers': 'localhost:9092',
'acks': 'all', # Wait for all in-sync replicas
'compression.type': 'gzip', # Compress messages
'enable.idempotence': True, # Exactly-once semantics per partition
'linger.ms': 20, # Small batching window
'retries': 3
})
def delivery_report(err, msg):
"""Called once per message, on success or permanent failure."""
if err is not None:
print(f"Failed to send event: {err}")
else:
print(f"Sent to partition {msg.partition()} at offset {msg.offset()}")
# Send user events
def send_user_event(user_id, action, metadata=None):
event = {
'event_id': str(uuid.uuid4()),
'user_id': user_id,
'action': action,
'timestamp': datetime.now(timezone.utc).isoformat(),
'metadata': metadata or {}
}
# Send to topic with user_id as key for partitioning
producer.produce(
'user-events',
key=str(user_id),
value=json.dumps(event).encode('utf-8'),
on_delivery=delivery_report
)
# Serve delivery callbacks without blocking the producer
producer.poll(0)
# Usage
send_user_event(12345, 'login', {'ip': '192.168.1.100', 'device': 'mobile'})
send_user_event(12345, 'view_product', {'product_id': 'prod_789', 'category': 'electronics'})
send_user_event(12345, 'add_to_cart', {'product_id': 'prod_789', 'quantity': 1})
# Block until every queued message has been delivered
producer.flush(10)Kafka Consumer (Python)
The same library handles the consume side. Poll in a loop, decode each message as one JSON object, and commit offsets only after the event is safely processed.
from confluent_kafka import Consumer, KafkaError
import json
# Create consumer
consumer = Consumer({
'bootstrap.servers': 'localhost:9092',
'group.id': 'event-processor-group',
'auto.offset.reset': 'earliest', # Start from beginning if no offset
'enable.auto.commit': False, # Commit only after successful processing
'max.poll.interval.ms': 300000
})
consumer.subscribe(['user-events'])
def process_purchase(event):
# Send confirmation email
# Update inventory
# Record in database
pass
def update_cart(event):
# Update user's shopping cart
pass
def track_analytics(event):
# Send to analytics platform
pass
# Process events in real-time
try:
while True:
message = consumer.poll(timeout=1.0)
if message is None:
continue
if message.error():
if message.error().code() == KafkaError._PARTITION_EOF:
continue
raise Exception(message.error())
event = json.loads(message.value().decode('utf-8'))
print(f"Processing event: {event['action']} for user {event['user_id']}")
print(f"Partition: {message.partition()}, Offset: {message.offset()}")
# Process based on action type
if event['action'] == 'purchase':
process_purchase(event)
elif event['action'] == 'add_to_cart':
update_cart(event)
elif event['action'] == 'view_product':
track_analytics(event)
# At-least-once delivery: commit after the work is done
consumer.commit(message=message, asynchronous=False)
finally:
consumer.close()Legacy reference only: kafka-python (do not use for new work)
Shown purely so you can recognise it in older code and migrate away from it. The consumer group id, topic and offset-reset settings map one-for-one onto the confluent-kafka configuration above.
# DEPRECATED - kafka-python is effectively unmaintained.
# Migrate this to confluent_kafka.Consumer (see above).
from kafka import KafkaConsumer
import json
consumer = KafkaConsumer(
'user-events',
bootstrap_servers=['localhost:9092'],
value_deserializer=lambda m: json.loads(m.decode('utf-8')),
group_id='event-processor-group',
auto_offset_reset='earliest'
)
for message in consumer:
event = message.value
print(f"Processing event: {event['action']} for user {event['user_id']}")Kafka Streams (Java)
Stream processing with stateful transformations and aggregations.
import org.apache.kafka.streams.KafkaStreams;
import org.apache.kafka.streams.StreamsBuilder;
import org.apache.kafka.streams.kstream.*;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
public class UserEventProcessor {
public static void main(String[] args) {
StreamsBuilder builder = new StreamsBuilder();
ObjectMapper mapper = new ObjectMapper();
// Read events stream
KStream<String, String> events = builder.stream("user-events");
// Parse JSON and filter
KStream<String, JsonNode> parsedEvents = events
.mapValues(value -> {
try {
return mapper.readTree(value);
} catch (Exception e) {
return null;
}
})
.filter((key, value) -> value != null);
// Count purchases by user (windowed aggregation)
KTable<Windowed<String>, Long> purchaseCounts = parsedEvents
.filter((key, value) -> value.get("action").asText().equals("purchase"))
.groupBy((key, value) -> value.get("user_id").asText())
.windowedBy(TimeWindows.of(Duration.ofMinutes(5)))
.count();
// Alert on high-value purchases
parsedEvents
.filter((key, value) ->
value.get("action").asText().equals("purchase") &&
value.get("amount").asDouble() > 1000.0
)
.to("high-value-purchases");
// Calculate average cart value
parsedEvents
.filter((key, value) -> value.get("action").asText().equals("add_to_cart"))
.groupByKey()
.aggregate(
() -> new CartAggregate(),
(key, value, aggregate) -> aggregate.add(value),
Materialized.as("cart-aggregates")
);
KafkaStreams streams = new KafkaStreams(builder.build(), config);
streams.start();
}
}KafkaJS (Node.js)
const { Kafka } = require('kafkajs');
const kafka = new Kafka({
clientId: 'my-app',
brokers: ['localhost:9092']
});
// Producer
const producer = kafka.producer();
await producer.connect();
async function sendEvent(topic, key, event) {
await producer.send({
topic,
messages: [
{
key,
value: JSON.stringify(event),
headers: {
'correlation-id': event.correlationId,
},
},
],
});
}
// Consumer
const consumer = kafka.consumer({ groupId: 'event-processors' });
await consumer.connect();
await consumer.subscribe({ topic: 'user-events', fromBeginning: false });
await consumer.run({
eachMessage: async ({ topic, partition, message }) => {
const event = JSON.parse(message.value.toString());
console.log({
partition,
offset: message.offset,
value: event,
});
// Process event
await processEvent(event);
},
});
async function processEvent(event) {
switch (event.action) {
case 'purchase':
await handlePurchase(event);
break;
case 'refund':
await handleRefund(event);
break;
default:
console.log(`Unknown action: ${event.action}`);
}
}Kafka Connect JSONL Sink
Export Kafka topics to JSONL files for archival or batch processing.
{
"name": "jsonl-file-sink",
"config": {
"connector.class": "FileStreamSink",
"tasks.max": "1",
"file": "/data/events.jsonl",
"topics": "user-events",
"key.converter": "org.apache.kafka.connect.storage.StringConverter",
"value.converter": "org.apache.kafka.connect.json.JsonConverter",
"value.converter.schemas.enable": "false"
}
}
# Load connector
curl -X POST http://localhost:8083/connectors \
-H "Content-Type: application/json" \
-d @jsonl-sink-connector.json
# Result: Continuous JSONL export
# /data/events.jsonl contains:
{"event_id": "evt_001", "user_id": 123, "action": "login", "timestamp": "2026-01-15T10:00:00Z"}
{"event_id": "evt_002", "user_id": 124, "action": "view", "timestamp": "2026-01-15T10:00:05Z"}
{"event_id": "evt_003", "user_id": 123, "action": "purchase", "timestamp": "2026-01-15T10:01:00Z"}AWS Kinesis Data Streams
Kinesis Producer
import boto3
import json
from datetime import datetime
kinesis = boto3.client('kinesis', region_name='us-east-1')
stream_name = 'user-events-stream'
def put_record(data, partition_key):
"""Put single record to Kinesis"""
response = kinesis.put_record(
StreamName=stream_name,
Data=json.dumps(data),
PartitionKey=partition_key
)
return response['SequenceNumber']
def put_records_batch(records):
"""Batch put up to 500 records"""
kinesis_records = [
{
'Data': json.dumps(record['data']),
'PartitionKey': record['partition_key']
}
for record in records
]
response = kinesis.put_records(
StreamName=stream_name,
Records=kinesis_records
)
# Check for failures
failed_count = response['FailedRecordCount']
if failed_count > 0:
print(f"Failed to put {failed_count} records")
# Send events
event = {
'event_id': 'evt_12345',
'user_id': 98765,
'action': 'purchase',
'product_id': 'prod_456',
'amount': 99.99,
'timestamp': datetime.utcnow().isoformat()
}
sequence_number = put_record(event, partition_key=str(event['user_id']))
print(f"Event sent with sequence number: {sequence_number}")
# Batch sending for better throughput
events = [
{'data': {'action': 'login', 'user_id': i}, 'partition_key': str(i)}
for i in range(100)
]
put_records_batch(events)Kinesis Consumer
import boto3
import json
import time
kinesis = boto3.client('kinesis', region_name='us-east-1')
stream_name = 'user-events-stream'
def get_shard_iterator(shard_id, iterator_type='LATEST'):
"""Get iterator for a shard"""
response = kinesis.get_shard_iterator(
StreamName=stream_name,
ShardId=shard_id,
ShardIteratorType=iterator_type
)
return response['ShardIterator']
def process_records():
"""Process records from all shards"""
# Get all shards
response = kinesis.describe_stream(StreamName=stream_name)
shards = response['StreamDescription']['Shards']
for shard in shards:
shard_id = shard['ShardId']
shard_iterator = get_shard_iterator(shard_id)
while True:
response = kinesis.get_records(
ShardIterator=shard_iterator,
Limit=100
)
records = response['Records']
for record in records:
data = json.loads(record['Data'])
print(f"Processing: {data}")
# Process event
handle_event(data)
# Get next iterator
shard_iterator = response['NextShardIterator']
if not shard_iterator:
break
# Rate limiting
time.sleep(1)
def handle_event(event):
"""Process individual event"""
action = event.get('action')
if action == 'purchase':
process_purchase(event)
elif action == 'refund':
process_refund(event)Kinesis Data Firehose
Automatically deliver streaming data to S3, Redshift, or Elasticsearch as JSONL.
import boto3
import json
firehose = boto3.client('firehose', region_name='us-east-1')
delivery_stream = 'events-to-s3'
def send_to_firehose(records):
"""Send records to Firehose for S3 delivery"""
firehose_records = [
{'Data': json.dumps(record) + '\n'} # Firehose adds newlines
for record in records
]
response = firehose.put_record_batch(
DeliveryStreamName=delivery_stream,
Records=firehose_records
)
return response['FailedPutCount']
# Send events
events = [
{'user_id': 123, 'action': 'login', 'timestamp': '2026-01-15T10:00:00Z'},
{'user_id': 124, 'action': 'purchase', 'amount': 49.99, 'timestamp': '2026-01-15T10:01:00Z'}
]
failed = send_to_firehose(events)
print(f"Failed records: {failed}")
# Firehose Configuration (Terraform)
resource "aws_kinesis_firehose_delivery_stream" "events" {
name = "events-to-s3"
destination = "s3"
s3_configuration {
role_arn = aws_iam_role.firehose.arn
bucket_arn = aws_s3_bucket.events.arn
prefix = "events/year=!{timestamp:yyyy}/month=!{timestamp:MM}/day=!{timestamp:dd}/"
# JSONL output format
cloudwatch_logging_options {
enabled = true
}
}
}Change Data Capture (CDC)
Debezium CDC to Kafka
Stream database changes as JSONL events for real-time data synchronization.
# Debezium PostgreSQL Connector Configuration
{
"name": "postgres-cdc-connector",
"config": {
"connector.class": "io.debezium.connector.postgresql.PostgresConnector",
"database.hostname": "postgres.example.com",
"database.port": "5432",
"database.user": "debezium",
"database.password": "secret",
"database.dbname": "myapp",
"database.server.name": "myapp-db",
"table.include.list": "public.users,public.orders",
"plugin.name": "pgoutput",
# JSON output format
"key.converter": "org.apache.kafka.connect.json.JsonConverter",
"value.converter": "org.apache.kafka.connect.json.JsonConverter",
"value.converter.schemas.enable": "false",
# Kafka topic configuration
"topic.prefix": "cdc",
"transforms": "unwrap",
"transforms.unwrap.type": "io.debezium.transforms.ExtractNewRecordState",
"transforms.unwrap.drop.tombstones": "false"
}
}
# Example CDC event format
{
"before": null,
"after": {
"id": 12345,
"email": "[email protected]",
"name": "John Doe",
"created_at": "2026-01-15T10:00:00Z"
},
"source": {
"version": "1.9.0",
"connector": "postgresql",
"name": "myapp-db",
"ts_ms": 1705315200000,
"db": "myapp",
"schema": "public",
"table": "users"
},
"op": "c", // c=create, u=update, d=delete
"ts_ms": 1705315200000
}CDC Consumer
Read the change topic with confluent-kafka-python and branch on the Debezium operation code.
from confluent_kafka import Consumer
import json
consumer = Consumer({
'bootstrap.servers': 'localhost:9092',
'group.id': 'cdc-processor',
'auto.offset.reset': 'earliest',
'enable.auto.commit': False
})
consumer.subscribe(['cdc.public.users'])
while True:
message = consumer.poll(timeout=1.0)
if message is None or message.error():
continue
# Debezium tombstones (deletes) carry a null value - skip them
if message.value() is None:
continue
event = json.loads(message.value().decode('utf-8'))
operation = event['op']
table = event['source']['table']
if operation == 'c': # Create
handle_insert(event['after'])
elif operation == 'u': # Update
handle_update(event['before'], event['after'])
elif operation == 'd': # Delete
handle_delete(event['before'])
consumer.commit(message=message, asynchronous=False)
def handle_insert(record):
"""Sync new record to search index, cache, etc."""
print(f"New record: {record}")
# Update Elasticsearch
# Invalidate cache
# Send to analytics
def handle_update(before, after):
"""Handle record updates"""
print(f"Updated: {before} -> {after}")
# Track field changes
# Update downstream systems
def handle_delete(record):
"""Handle record deletions"""
print(f"Deleted: {record}")
# Remove from search index
# Archive for complianceCDC Use Cases
Database Replication
Real-time replication from PostgreSQL to MongoDB, keeping multiple databases in sync without custom code.
Search Index Sync
Automatically update Elasticsearch whenever database records change, ensuring search results are always current.
Cache Invalidation
Invalidate Redis cache entries when underlying data changes, preventing stale data issues.
Audit Logging
Capture all database changes for compliance, debugging, and analytics without application code changes.
Event Sourcing
Event Store with JSONL
Store immutable events as JSONL for complete system history and state reconstruction.
# Event Store Format
{"event_id": "evt_001", "aggregate_id": "order_12345", "event_type": "OrderCreated", "timestamp": "2026-01-15T10:00:00Z", "data": {"user_id": 98765, "items": [{"product_id": "prod_1", "quantity": 2}], "total": 99.99}, "version": 1}
{"event_id": "evt_002", "aggregate_id": "order_12345", "event_type": "OrderItemAdded", "timestamp": "2026-01-15T10:01:00Z", "data": {"product_id": "prod_2", "quantity": 1, "price": 49.99}, "version": 2}
{"event_id": "evt_003", "aggregate_id": "order_12345", "event_type": "OrderConfirmed", "timestamp": "2026-01-15T10:05:00Z", "data": {"confirmation_id": "conf_abc123", "payment_method": "credit_card"}, "version": 3}
{"event_id": "evt_004", "aggregate_id": "order_12345", "event_type": "OrderShipped", "timestamp": "2026-01-15T14:00:00Z", "data": {"tracking_number": "TRACK123", "carrier": "UPS"}, "version": 4}
# Python Event Store Implementation
import json
from datetime import datetime
from typing import List, Dict
class EventStore:
def __init__(self, filepath='events.jsonl'):
self.filepath = filepath
def append_event(self, aggregate_id: str, event_type: str, data: Dict, version: int):
"""Append event to store"""
event = {
'event_id': str(uuid.uuid4()),
'aggregate_id': aggregate_id,
'event_type': event_type,
'timestamp': datetime.utcnow().isoformat(),
'data': data,
'version': version
}
with open(self.filepath, 'a') as f:
f.write(json.dumps(event) + '\n')
return event['event_id']
def get_events(self, aggregate_id: str) -> List[Dict]:
"""Get all events for an aggregate"""
events = []
with open(self.filepath, 'r') as f:
for line in f:
event = json.loads(line)
if event['aggregate_id'] == aggregate_id:
events.append(event)
return events
def rebuild_state(self, aggregate_id: str) -> Dict:
"""Reconstruct current state from events"""
events = self.get_events(aggregate_id)
state = {}
for event in events:
state = self.apply_event(state, event)
return state
def apply_event(self, state: Dict, event: Dict) -> Dict:
"""Apply event to state"""
event_type = event['event_type']
data = event['data']
if event_type == 'OrderCreated':
state = {
'order_id': event['aggregate_id'],
'user_id': data['user_id'],
'items': data['items'],
'total': data['total'],
'status': 'created'
}
elif event_type == 'OrderItemAdded':
state['items'].append({
'product_id': data['product_id'],
'quantity': data['quantity']
})
state['total'] += data['price']
elif event_type == 'OrderConfirmed':
state['status'] = 'confirmed'
state['confirmation_id'] = data['confirmation_id']
elif event_type == 'OrderShipped':
state['status'] = 'shipped'
state['tracking_number'] = data['tracking_number']
return state
# Usage
store = EventStore()
# Append events
store.append_event('order_12345', 'OrderCreated', {'user_id': 98765, 'items': [], 'total': 0}, 1)
store.append_event('order_12345', 'OrderItemAdded', {'product_id': 'prod_1', 'quantity': 2, 'price': 99.99}, 2)
store.append_event('order_12345', 'OrderConfirmed', {'confirmation_id': 'conf_123'}, 3)
# Rebuild current state
current_state = store.rebuild_state('order_12345')
print(current_state)Snapshots for Performance
class EventStoreWithSnapshots(EventStore):
def __init__(self, filepath='events.jsonl', snapshot_interval=100):
super().__init__(filepath)
self.snapshot_file = filepath.replace('.jsonl', '_snapshots.jsonl')
self.snapshot_interval = snapshot_interval
def get_latest_snapshot(self, aggregate_id: str):
"""Get most recent snapshot"""
snapshots = []
try:
with open(self.snapshot_file, 'r') as f:
for line in f:
snapshot = json.loads(line)
if snapshot['aggregate_id'] == aggregate_id:
snapshots.append(snapshot)
except FileNotFoundError:
return None
return snapshots[-1] if snapshots else None
def save_snapshot(self, aggregate_id: str, state: Dict, version: int):
"""Save state snapshot"""
snapshot = {
'aggregate_id': aggregate_id,
'state': state,
'version': version,
'timestamp': datetime.utcnow().isoformat()
}
with open(self.snapshot_file, 'a') as f:
f.write(json.dumps(snapshot) + '\n')
def rebuild_state(self, aggregate_id: str) -> Dict:
"""Rebuild from latest snapshot + subsequent events"""
snapshot = self.get_latest_snapshot(aggregate_id)
if snapshot:
state = snapshot['state']
from_version = snapshot['version']
else:
state = {}
from_version = 0
# Apply events after snapshot
events = self.get_events(aggregate_id)
events_to_apply = [e for e in events if e['version'] > from_version]
for event in events_to_apply:
state = self.apply_event(state, event)
# Create new snapshot if needed
if len(events) % self.snapshot_interval == 0:
self.save_snapshot(aggregate_id, state, len(events))
return stateReal-Time Streaming to Clients
WebSocket JSONL Streaming
// Server (Node.js with ws)
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
wss.on('connection', (ws) => {
console.log('Client connected');
// Stream events to client as JSONL
const interval = setInterval(() => {
const event = {
type: 'stock_update',
symbol: 'AAPL',
price: (Math.random() * 200).toFixed(2),
timestamp: new Date().toISOString()
};
// Send as JSON string (one per message)
ws.send(JSON.stringify(event));
}, 1000);
ws.on('close', () => {
clearInterval(interval);
console.log('Client disconnected');
});
});
// Client (Browser)
const ws = new WebSocket('ws://localhost:8080');
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log('Received:', data);
// Update UI with real-time data
updateStockPrice(data.symbol, data.price);
};
ws.onopen = () => {
console.log('Connected to stream');
};
ws.onerror = (error) => {
console.error('WebSocket error:', error);
};Server-Sent Events (SSE)
// Server (Express)
const express = require('express');
const app = express();
app.get('/events', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
// Send events every second
const interval = setInterval(() => {
const event = {
id: Date.now(),
type: 'update',
data: {
metric: 'active_users',
value: Math.floor(Math.random() * 1000)
}
};
// SSE format: data: JSON\n\n
res.write(`data: ${JSON.stringify(event)}\n\n`);
}, 1000);
req.on('close', () => {
clearInterval(interval);
});
});
app.listen(3000);
// Client (Browser)
const eventSource = new EventSource('/events');
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log('Event received:', data);
// Update dashboard
updateMetric(data.data.metric, data.data.value);
};
eventSource.onerror = (error) => {
console.error('SSE error:', error);
eventSource.close();
};Stream Processing Frameworks
Apache Flink
FlinkKafkaConsumer was deprecated in Flink 1.14 and removed in Flink 1.17. Current jobs build a KafkaSource and hand it to env.fromSource() together with a watermark strategy. When migrating an existing job, use setStartingOffsets(OffsetsInitializer.committedOffsets()) so the new source picks up exactly where the old consumer group left off.
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.api.common.serialization.SimpleStringSchema;
import org.apache.flink.connector.kafka.source.KafkaSource;
import org.apache.flink.connector.kafka.source.enumerator.initializer.OffsetsInitializer;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.api.common.functions.RichMapFunction;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.windowing.assigners.TumblingEventTimeWindows;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.time.Duration;
public class EventProcessor {
// ObjectMapper is not serializable, so build it on the task manager.
public static class ParseJson extends RichMapFunction<String, JsonNode> {
private transient ObjectMapper mapper;
@Override
public void open(Configuration parameters) {
mapper = new ObjectMapper();
}
@Override
public JsonNode map(String line) throws Exception {
return mapper.readTree(line);
}
}
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
// KafkaSource replaces FlinkKafkaConsumer (deprecated 1.14, removed 1.17).
KafkaSource<String> source = KafkaSource.<String>builder()
.setBootstrapServers("localhost:9092")
.setTopics("user-events")
.setGroupId("flink-consumer")
// Migration: resume from the offsets the old consumer group committed.
// Use OffsetsInitializer.earliest() for a fresh full replay instead.
.setStartingOffsets(OffsetsInitializer.committedOffsets())
.setValueOnlyDeserializer(new SimpleStringSchema())
.build();
DataStream<String> stream = env.fromSource(
source,
WatermarkStrategy.<String>forBoundedOutOfOrderness(Duration.ofSeconds(5)),
"kafka-user-events"
);
// Parse one JSON object per line
DataStream<JsonNode> events = stream
.map(new ParseJson())
.filter(event -> "purchase".equals(event.get("action").asText()));
// Window aggregation, then alert on anomalies
events
.keyBy(event -> event.get("user_id").asText())
.window(TumblingEventTimeWindows.of(Duration.ofMinutes(5)))
.aggregate(new RevenueAggregator())
.filter(revenue -> revenue.getTotal() > 10000)
.sinkTo(new AlertSink());
env.execute("Event Processor");
}
}Apache Spark Structured Streaming
from pyspark.sql import SparkSession
from pyspark.sql.functions import *
from pyspark.sql.types import *
spark = SparkSession.builder.appName("EventProcessor").getOrCreate()
# Read from Kafka as streaming DataFrame
events_df = spark \
.readStream \
.format("kafka") \
.option("kafka.bootstrap.servers", "localhost:9092") \
.option("subscribe", "user-events") \
.load()
# Parse JSON
schema = StructType([
StructField("user_id", IntegerType()),
StructField("action", StringType()),
StructField("amount", DoubleType()),
StructField("timestamp", StringType())
])
parsed_df = events_df \
.selectExpr("CAST(value AS STRING) as json") \
.select(from_json(col("json"), schema).alias("data")) \
.select("data.*")
# Aggregate by window
windowed_revenue = parsed_df \
.withWatermark("timestamp", "10 minutes") \
.groupBy(
window(col("timestamp"), "5 minutes"),
col("user_id")
) \
.agg(
sum("amount").alias("total_amount"),
count("*").alias("purchase_count")
)
# Write to console
query = windowed_revenue \
.writeStream \
.outputMode("update") \
.format("console") \
.start()
query.awaitTermination()Modern Streaming Ecosystem
Kafka and Kinesis are no longer the only sensible homes for a JSONL event stream. Three projects turn up constantly in new builds, and all three treat a stream as an ordered sequence of self-contained JSON documents - exactly the JSONL model.
Redpanda (Kafka API Compatible)
Redpanda speaks the Kafka wire protocol, so every client on this page keeps working unchanged - including the confluent-kafka producer above. The difference is operational: one C++ binary, no JVM and no ZooKeeper. Its CLI, rpk, pipes JSONL files in and out of a topic directly, which makes it a genuinely pleasant local test harness.
# Create a topic with 6 partitions
rpk topic create user-events -p 6
# Produce JSONL: one JSON object per line, straight from a file
rpk topic produce user-events < events.jsonl
# Consume it back as JSONL (values only, no envelope)
rpk topic consume user-events --format '%v\n' --num 3
{"event_id": "evt_001", "user_id": 123, "action": "login"}
{"event_id": "evt_002", "user_id": 124, "action": "view"}
{"event_id": "evt_003", "user_id": 123, "action": "purchase"}NATS JetStream
JetStream adds durable, replayable streams to NATS while keeping the footprint tiny, which suits edge and IoT fleets where a Kafka cluster is overkill. Subjects are hierarchical, messages are opaque bytes, and one JSON document per message replays cleanly to a JSONL file.
# Create a persistent, file-backed stream
nats stream add EVENTS \
--subjects "events.>" \
--storage file \
--retention limits \
--max-age 168h \
--dupe-window 2m
# Publish one JSON document per message
while read -r line; do nats pub events.user "$line"; done < events.jsonl
# Durable pull consumer, replayed straight back out as JSONL
nats consumer add EVENTS processor --pull --ack explicit --deliver all
nats consumer next EVENTS processor --count 100 --raw >> replay.jsonlRedpanda Connect (formerly Benthos)
Most JSONL stream work is filter, reshape, enrich, forward. Redpanda Connect expresses that as a YAML pipeline with a mapping language called Bloblang, so a transform that would be a hundred lines of consumer code becomes a config file you can review in a pull request.
# connect.yaml - JSONL file in, filtered and enriched, out to a topic
input:
file:
paths: [ /data/events.jsonl ]
scanner:
lines: {}
pipeline:
processors:
# Drop noise before it costs anything downstream
- mapping: 'root = if this.action == "heartbeat" { deleted() }'
# Normalise and enrich every surviving line
- mapping: |
root = this
root.user_id = this.user_id.string()
root.amount = this.amount.number().catch(0)
root.ingested_at = now()
output:
kafka_franz:
seed_brokers: [ localhost:9092 ]
topic: user-events
key: '${! json("user_id") }'
# Run it
rpk connect run connect.yamlBest Practices
Event Design
- Include unique event ID and timestamp in every event
- Use clear, descriptive event types (UserLoggedIn vs Action1)
- Include all context needed to process event independently
- Use consistent field names across all events
- Version your event schemas for backward compatibility
Performance
- Batch events when possible (Kafka batch size, Kinesis PutRecords)
- Enable compression (gzip, snappy, lz4) for network efficiency
- Use appropriate partition keys for even load distribution
- Monitor consumer lag and scale accordingly
- Implement backpressure handling for slow consumers
Reliability
- Implement idempotent consumers to handle duplicate events
- Use at-least-once delivery with deduplication
- Set up dead letter queues for failed events
- Implement retry logic with exponential backoff
- Monitor and alert on processing failures
Data Management
- Define retention policies for stream data
- Archive events to cold storage (S3, Glacier) for compliance
- Implement data quality checks on ingestion
- Use schema registry for event validation
- Document event formats and maintain changelog