Why This Matters
Amazon Kinesis is the foundation of real-time data engineering on AWS. While batch processing handles historical data, Kinesis enables sub-second processing of live data streams from applications, IoT devices, logs, and clickstreams. Understanding shard capacity, partition key design, and consumer patterns is essential for building reliable streaming pipelines. Kinesis Data Firehose provides serverless delivery to S3, Redshift, and OpenSearch without managing infrastructure.
Key Insight: Kinesis Data Streams and Kinesis Data Firehose serve different purposes. KDS is for custom processing with shards and consumers. KDF is for serverless delivery to AWS services. Choosing the wrong one can lead to unnecessary complexity or cost.
Kinesis Architecture
Kinesis Data Streams (KDS)
KDS is the foundation of the Kinesis platform, providing a durable, scalable stream for custom processing.
Shard Capacity
| Metric | Per Shard | Calculation Example |
|---|---|---|
| Writeεει | 1 MB/sec, 1,000 records/sec | 10 shards = 10 MB/s write |
| Readεει | 2 MB/sec, 5 GetRecords/sec | 10 shards = 20 MB/s read |
| Enhanced Fan-Out | 2 MB/sec per consumer | 5 consumers = 10 MB/s per consumer |
| Retention | 24h default, 365 days max | Extended retention at additional cost |
Shard Calculation Formula
Write_Shard_Needed = ceil(Incoming_Data_Rate_MB_per_sec / 1)
Record_Shard_Needed = ceil(Incoming_Records_per_sec / 1000)
Shards_Required = max(Write_Shard_Needed, Record_Shard_Needed)
Example:
5 MB/sec incoming, 4,000 records/sec
Write shards: ceil(5 / 1) = 5
Record shards: ceil(4000 / 1000) = 4
Required shards: max(5, 4) = 5 shards
Production Code: Kinesis Operations
Create Kinesis Stream with Boto3
import boto3
import json
import logging
import time
from datetime import datetime
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def create_kinesis_stream(
stream_name: str,
shard_count: int = 4,
retention_hours: int = 24,
enable_enhanced_fan_out: bool = False
) -> dict:
"""
Create a Kinesis Data Stream with production-grade settings.
Args:
stream_name: Unique stream identifier
shard_count: Number of shards for throughput
retention_hours: Data retention period (24-8760 hours)
enable_enhanced_fan_out: Enable enhanced fan-out for consumers
Returns:
dict with stream details
"""
kinesis_client = boto3.client('kinesis')
stream_config = {
'StreamName': stream_name,
'ShardCount': shard_count,
'StreamModeDetails': {
'StreamMode': 'PROVISIONED'
},
'Tags': {
'Environment': 'production',
'ManagedBy': 'boto3'
}
}
try:
response = kinesis_client.create_stream(**stream_config)
logger.info(f"Created stream: {stream_name} with {shard_count} shards")
# Wait for stream to become active
waiter = kinesis_client.get_waiter('stream_exists')
logger.info("Waiting for stream to become active...")
waiter.wait(StreamName=stream_name)
# Update retention period if needed
if retention_hours > 24:
kinesis_client.increase_stream_retention_period(
StreamName=stream_name,
RetentionPeriodHours=retention_hours
)
logger.info(f"Increased retention to {retention_hours} hours")
# Enable enhanced fan-out if requested
if enable_enhanced_fan_out:
register_enhanced_consumer(stream_name, f"{stream_name}-consumer")
return {
'stream_name': stream_name,
'shard_count': shard_count,
'retention_hours': retention_hours,
'status': 'ACTIVE'
}
except kinesis_client.exceptions.ResourceInUseException:
logger.warning(f"Stream {stream_name} already exists")
return describe_stream(stream_name)
except Exception as e:
logger.error(f"Failed to create stream: {str(e)}")
raise
def put_records_to_stream(
stream_name: str,
records: list,
partition_key_extractor: callable = None
) -> dict:
"""
Put records to a Kinesis stream with error handling.
Args:
stream_name: Target stream name
records: List of record dictionaries with 'data' and optional 'partition_key'
partition_key_extractor: Function to extract partition key from record
Returns:
dict with success count and failed records
"""
kinesis_client = boto3.client('kinesis')
formatted_records = []
for i, record in enumerate(records):
partition_key = record.get('partition_key')
if not partition_key and partition_key_extractor:
partition_key = partition_key_extractor(record)
elif not partition_key:
partition_key = f"pk-{i}"
formatted_records.append({
'Data': json.dumps(record['data']).encode('utf-8'),
'PartitionKey': partition_key
})
failed_records = []
success_count = 0
# Process in batches of 500 (Kinesis limit)
for i in range(0, len(formatted_records), 500):
batch = formatted_records[i:i + 500]
try:
response = kinesis_client.put_records(
StreamName=stream_name,
Records=batch
)
success_count += response['Records'].__len__() - response['FailedRecordCount']
failed_records.extend([
{'index': i + idx, 'error_code': r.get('ErrorCode'), 'error_msg': r.get('ErrorMessage')}
for idx, r in enumerate(response['Records'])
if 'ErrorCode' in r
])
if response['FailedRecordCount'] > 0:
logger.warning(f"Batch {i // 500}: {response['FailedRecordCount']} records failed")
except Exception as e:
logger.error(f"Batch {i // 500} failed completely: {str(e)}")
failed_records.extend([{'index': i + j, 'error': str(e)} for j in range(len(batch))])
result = {
'total_records': len(records),
'success_count': success_count,
'failed_count': len(failed_records),
'failed_records': failed_records
}
logger.info(f"Put {success_count}/{len(records)} records to {stream_name}")
return result
def register_enhanced_consumer(stream_name: str, consumer_name: str) -> str:
"""Register an enhanced fan-out consumer."""
kinesis_client = boto3.client('kinesis')
response = kinesis_client.register_stream_consumer(
StreamName=stream_name,
ConsumerName=consumer_name
)
consumer_arn = response['Consumer']['ConsumerARN']
logger.info(f"Registered enhanced consumer: {consumer_arn}")
return consumer_arn
def describe_stream(stream_name: str) -> dict:
"""Get stream details including shard information."""
kinesis_client = boto3.client('kinesis')
response = kinesis_client.describe_stream(StreamName=stream_name)
stream_desc = response['StreamDescription']
return {
'stream_name': stream_desc['StreamName'],
'stream_arn': stream_desc['StreamARN'],
'shard_count': len(stream_desc['Shards']),
'retention_hours': stream_desc['RetentionPeriodHours'],
'status': stream_desc['StreamStatus'],
'creation_time': str(stream_desc['StreamCreationTimestamp'])
}
def get_shard_iterator(stream_name: str, shard_id: str, iterator_type: str = 'LATEST') -> str:
"""Get a shard iterator for reading records."""
kinesis_client = boto3.client('kinesis')
response = kinesis_client.get_shard_iterator(
StreamName=stream_name,
ShardId=shard_id,
ShardIteratorType=iterator_type
)
return response['ShardIterator']
Kinesis Data Firehose
Firehose is a fully managed service for delivering streaming data to AWS data stores.
Delivery Destinations
| Destination | Format Support | Buffer Settings |
|---|---|---|
| S3 | Parquet, ORC, Avro, JSON, CSV | 1-128 MB, 60-900 sec |
| Redshift | COPY via S3 staging | 1-128 MB, 60-900 sec |
| OpenSearch | JSON | 1-100 MB, 60-900 sec |
| HTTP Endpoints | JSON, Avro, Parquet | 1-128 MB, 60-900 sec |
Firehose Configuration Example
import boto3
import json
import logging
logger = logging.getLogger(__name__)
def create_firehose_delivery_stream(
stream_name: str,
s3_destination: str,
iam_role_arn: str,
buffer_size_mb: int = 5,
buffer_interval_sec: int = 300,
compression: str = "GZIP",
format_conversion: str = "PARQUET"
) -> dict:
"""
Create a Kinesis Data Firehose delivery stream.
Args:
stream_name: Delivery stream name
s3_destination: S3 bucket ARN for delivery
iam_role_arn: IAM role ARN for Firehose
buffer_size_mb: Buffer size in MB (1-128)
buffer_interval_sec: Buffer interval in seconds (60-900)
compression: Compression format (GZIP, SNAPPY, ZIP)
format_conversion: Target format (PARQUET, ORC, AVRO)
Returns:
dict with delivery stream details
"""
firehose_client = boto3.client('firehose')
s3_config = {
'RoleARN': iam_role_arn,
'BucketARN': s3_destination,
'Prefix': 'raw/year=!{timestamp:yyyy}/month=!{timestamp:MM}/day=!{timestamp:dd}/',
'ErrorOutputPrefix': 'errors/',
'BufferingHints': {
'SizeInMBs': buffer_size_mb,
'IntervalInSeconds': buffer_interval_sec
},
'CompressionFormat': compression,
'CloudWatchLoggingOptions': {
'Enabled': True,
'LogGroupName': '/aws/kinesis-firehose',
'LogStreamName': stream_name
}
}
if format_conversion:
s3_config['ProcessingConfiguration'] = {
'Enabled': True,
'Processors': [{
'Type': 'ConvertRecord',
'Parameters': [{
'ParameterName': 'OutputFormat',
'ParameterValue': format_conversion
}]
}]
}
try:
response = firehose_client.create_delivery_stream(
DeliveryStreamName=stream_name,
DeliveryStreamType='DirectPut',
S3DestinationConfiguration=s3_config,
Tags={
'Environment': 'production',
'ManagedBy': 'boto3'
}
)
stream_arn = response['DeliveryStreamARN']
logger.info(f"Created Firehose stream: {stream_arn}")
return {'stream_arn': stream_arn, 'status': 'CREATING'}
except firehose_client.exceptions.ResourceInUseException:
logger.warning(f"Firehose stream {stream_name} already exists")
return describe_firehose_stream(stream_name)
except Exception as e:
logger.error(f"Failed to create Firehose stream: {str(e)}")
raise
def describe_firehose_stream(stream_name: str) -> dict:
"""Get Firehose delivery stream details."""
firehose_client = boto3.client('firehose')
response = firehose_client.describe_delivery_stream(
DeliveryStreamName=stream_name
)
stream_desc = response['DeliveryStreamDescription']
return {
'stream_name': stream_desc['DeliveryStreamName'],
'stream_arn': stream_desc['DeliveryStreamARN'],
'status': stream_desc['DeliveryStreamStatus'],
'creation_time': str(stream_desc['CreateTimestamp']),
'destinations': len(stream_desc.get('Destinations', []))
}
Real-World Project Structure
kinesis-streaming-project/
βββ producers/
β βββ web_app_producer.py
β βββ iot_producer.py
β βββ log_agent_config.json
β βββ kinesis_agent_config.json
βββ streams/
β βββ data_streams/
β β βββ create_streams.py
β β βββ reshard_stream.py
β βββ firehose/
β βββ create_delivery_streams.py
β βββ firehose_config.json
βββ consumers/
β βββ lambda_consumer/
β β βββ handler.py
β β βββ requirements.txt
β βββ kcl_consumer/
β β βββ consumer.py
β β βββ config.properties
β βββ flink_consumer/
β βββ streaming_job.py
β βββ pom.xml
βββ analytics/
β βββ kda_sql_app.sql
β βββ kda_flink_app.py
β βββ window_functions.sql
βββ infrastructure/
β βββ cloudformation/
β β βββ kinesis_streams.yaml
β β βββ kinesis_firehose.yaml
β β βββ iam_roles.yaml
β βββ terraform/
β βββ main.tf
β βββ streams.tf
β βββ variables.tf
βββ monitoring/
β βββ cloudwatch_dashboard.json
β βββ alarms.yaml
β βββ metrics_collector.py
βββ docs/
βββ architecture.md
βββ capacity_planning.md
βββ runbook.md
Mathematical Formations
Kinesis Cost Estimation
Data Streams Cost:
Shard_Cost = Shard_Count * $0.015/hr * Hours
PUT_Cost = (Total_PUT_Units / 25KB) * $0.00000025
Enhanced_Fan-Out = Consumer_Count * $0.015/hr * Hours
Example:
10 shards * $0.015 * 24 * 30 = $108/month (shards only)
1M PUTs/hour: (1,000,000 / 25,000) * $0.00000025 * 24 * 30 = $0.72/month
Firehose Cost:
Data_Cost = Data_Ingested_GB * $0.029 (first 10 TB/month)
Transform_Cost = Data_Transformed_GB * $0.006
Format_Conversion = Data_Converted_GB * $0.025
Example:
500 GB/day ingested: 0.5 * 30 * $0.029 = $0.435/month
Throughput Calculation
Max_Throughput = Shards * 1 MB/sec (write)
Max_Records = Shards * 1,000 records/sec
Required_Shards = ceil(Max(Data_Rate_MB, Records_Rate_KB / 1))
Example:
10 MB/sec incoming, 8,000 records/sec
Data shards: ceil(10) = 10
Record shards: ceil(8000/1000) = 8
Required: max(10, 8) = 10 shards
Performance Considerations
| Factor | Impact | Optimization Strategy |
|---|---|---|
| Shard Count | Directly limits throughput | Right-size based on data rate, use auto-scaling |
| Partition Key | Determines shard assignment | Choose high-cardinality keys for even distribution |
| Batch Size | Affects PUT latency | Use PutRecords with batches of 500 |
| Enhanced Fan-Out | Dedicated throughput per consumer | Use when multiple consumers read same stream |
| Buffer Size | Delivery latency vs efficiency | Larger buffers reduce cost but increase latency |
| Compression | Reduces data transfer costs | Use GZIP for JSON, Snappy for Avro |
| Lambda Transformations | Adds latency but enables enrichment | Keep Lambda functions efficient (< 1 second) |
Security Considerations
| Security Layer | Implementation | Priority |
|---|---|---|
| Encryption at Rest | KMS server-side encryption | Critical |
| Encryption in Transit | TLS 1.2 for all API calls | Critical |
| IAM Policies | Least-privilege for producers and consumers | Critical |
| VPC Endpoints | Private connectivity without internet | High |
| Resource Policies | Cross-account access control | High |
| CloudTrail | Log all Kinesis API calls | High |
| Stream ARN Restrictions | Limit access to specific streams | Medium |
| Tag-Based Access | Control access using resource tags | Medium |
Interview Questions & Answers
Q1: What is the difference between Kinesis Data Streams and Kinesis Data Firehose?
Answer: KDS is for custom processing - you manage shards, consumers, and processing logic. It provides 2MB/sec read throughput per shard with enhanced fan-out options. KDF is for serverless delivery - it automatically scales and delivers to S3, Redshift, or OpenSearch without managing infrastructure.
Use KDS when:
- You need custom processing logic
- Multiple consumers read from the same stream
- You need sub-second latency
- You want control over data ordering per shard
Use KDF when:
- You want serverless delivery to AWS services
- You can tolerate buffer delays (60-900 seconds)
- You want format conversion (JSON to Parquet)
- You prefer zero infrastructure management
Q2: How do you determine the number of shards needed?
Answer: Shard calculation formula:
- Write throughput: Divide expected data rate by 1 MB/sec per shard
- Record throughput: Divide expected records/sec by 1,000 records/sec per shard
- Required shards: Take the maximum of the two calculations
Example:
- Expected data rate: 5 MB/sec
- Expected records: 4,000 records/sec
- Write shards: ceil(5 / 1) = 5 shards
- Record shards: ceil(4000 / 1000) = 4 shards
- Required: max(5, 4) = 5 shards
Also consider:
- Peak vs average load (use average for on-demand mode)
- Enhanced fan-out adds 2MB/sec per consumer
- Use CloudWatch
IncomingBytesandIncomingRecordsto monitor
Q3: What is a partition key and why is it important?
Answer: A partition key determines which shard receives each record. Records with the same partition key always go to the same shard, ensuring ordering within that shard.
Importance:
- Data ordering: Records with the same key are processed in order
- Load distribution: Uneven keys cause hot shards
- Consumer design: Related data should use the same key
Best practices:
- Use high-cardinality keys (many unique values)
- Avoid low-cardinality keys (e.g., boolean, status codes)
- Use UUID or customer ID for even distribution
- Monitor
WriteProvisionedThroughputExceededfor hot shards
Q4: How does Kinesis handle data ordering?
Answer: Kinesis guarantees ordering at the shard level. Records with the same partition key are placed in the same shard and are delivered in order to consumers.
Key points:
- Ordering is per-shard, not across shards
- Partition key determines shard assignment
- Enhanced fan-out maintains ordering per consumer
- KCL (Kinesis Client Library) handles shard splitting and lease management
Limitations:
- No ordering across shards
- Shard splitting can temporarily break ordering
- Replaying records may affect ordering guarantees
Q5: What is the Kinesis Client Library (KCL)?
Answer: KCL is a library that simplifies building custom consumers for Kinesis streams. It handles:
- Shard discovery: Automatically discovers and tracks shards
- Lease management: Coordinates multiple consumers across shards
- Checkpoint tracking: Tracks processing progress for fault tolerance
- Load balancing: Distributes shards across consumer instances
- Heartbeat monitoring: Detects and replaces failed consumers
KCL implementations exist for Java, Python (amazon-kinesis-client), and other languages. It's essential for building production-grade consumers that need exactly-once processing semantics.
Q6: How do you handle duplicate records in Kinesis?
Answer: Kinesis provides at-least-once delivery by default. Duplicates can occur during network retries, shard splits, or consumer failures.
Strategies for handling duplicates:
- Idempotent processing: Design consumers to handle duplicates gracefully
- Deduplication tables: Use DynamoDB to track processed record IDs
- Sequence numbers: Use Kinesis sequence numbers for ordering
- Timestamps: Include event timestamps for time-based deduplication
- Consumer checkpointing: Use KCL checkpointing for consistent offsets
For exactly-once semantics, use KCL with DynamoDB-based lease management and implement idempotent writes.
Q7: What is enhanced fan-out and when would you use it?
Answer: Enhanced fan-out provides dedicated 2MB/sec read throughput per registered consumer, reducing contention compared to shared throughput.
Use enhanced fan-out when:
- Multiple consumers read from the same stream
- Each consumer needs consistent, dedicated throughput
- You want to avoid read throttling
- Low-latency processing is critical (sub-second)
Cost consideration:
- Standard reads: Shared 2MB/sec per shard
- Enhanced fan-out: $0.015 per consumer per hour per shard
- Worth it when 3+ consumers need dedicated throughput
Q8: How does Kinesis Data Analytics differ from Apache Flink?
Answer:
| Aspect | KDA SQL | KDA Flink | Self-Managed Flink |
|---|---|---|---|
| Language | Standard SQL | Java/Scala | Java/Scala |
| Complexity | Low | Medium | High |
| State Management | Managed by AWS | Managed by AWS | Self-managed |
| Scaling | Auto-scaling | Auto-scaling | Manual |
| Cost | Per KPU-hour | Per KPU-hour | EC2 costs |
Use KDA SQL for:
- Simple windowed aggregations
- SQL-based streaming analytics
- Quick prototyping
Use KDA Flink for:
- Complex event processing
- Custom operators and transformations
- Stateful computations
- Integration with external systems
Common Pitfalls
| Pitfall | Impact | Prevention |
|---|---|---|
| Insufficient shards | Write throttling, data loss | Monitor CloudWatch, use auto-scaling |
| Poor partition key | Hot shards, uneven load | Use high-cardinality keys, monitor shard usage |
| Missing error handling | Silent failures, data loss | Implement retry logic, use DLQ for Firehose |
| Ignoring iterator age | Consumer lag, data expiration | Monitor IteratorAge metric, scale consumers |
| Over-provisioning shards | Unnecessary cost | Use on-demand mode for variable workloads |
| No encryption | Security vulnerabilities | Enable KMS encryption for all streams |
| Missing monitoring | Issues discovered late | Set up CloudWatch alarms for key metrics |
| Firehose buffer too small | Higher costs, more S3 objects | Balance buffer size with latency requirements |
Why This Matters for Your Career
Kinesis expertise is essential for real-time data engineering roles. Understanding shard management, partition key design, and consumer patterns demonstrates your ability to build reliable streaming pipelines. Kinesis is the foundation for real-time analytics, IoT processing, and event-driven architectures. Mastering Kinesis concepts will enhance your candidacy for roles requiring streaming data expertise.
Key Takeaways
- KDS provides custom processing with shards and consumers for sub-second latency
- KDF provides serverless delivery to S3, Redshift, and OpenSearch
- Shard count directly limits throughput - right-size based on data rate
- Partition key design is critical for even distribution and ordering
- Enhanced fan-out provides dedicated throughput for multiple consumers
- At-least-once delivery requires idempotent consumer design