Siddhant Deval
Siddhant Deval
backend19 min read

AWS Streaming Primitives: Kinesis Data Streams and Amazon MSK

Kinesis Data Streams and Amazon MSK are AWS's managed implementations of the commit-log pattern — each trades configurability for managed operational burden. This article maps Kinesis shards to Kafka partitions, implements Enhanced Fan-Out for dedicated consumer throughput, and builds the decision model for choosing between Kinesis, MSK Provisioned, MSK Serverless, and self-managed Kafka.

AWS Streaming Primitives: Kinesis Data Streams and Amazon MSK

The analytics team needs a real-time clickstream pipeline ingesting 50,000 events per second from a web application, with two downstream consumers: a Lambda that feeds a live dashboard and a Flink job that produces hourly aggregations. The infrastructure team must choose between three options: self-managed Kafka on EC2, Amazon MSK, and Kinesis Data Streams. The Kafka team has expertise but no desire to manage broker VMs. Flink expects the Kafka API. But the dashboard Lambda team wants the simplest possible AWS-native integration.

This is the canonical MSK-vs-Kinesis decision — not a question of which is better, but which fits the team's API expectations, operational model, and throughput shape.

Architectural Note

Series positioning: This is Part 2 of Messaging at Cloud Scale. Both Kinesis and MSK implement the commit-log pattern from Series 1 Part 2 (Queues vs Logs). This article maps the Kafka concepts from Part 3 (Kafka Internals) to their AWS-managed equivalents. Prerequisite: familiarity with Kafka partitions, consumer groups, and the producer-broker-consumer lifecycle.


1. Kinesis Data Streams

1.1 Shards: The Kinesis Partition

A Kinesis shard is the direct equivalent of a Kafka partition — the unit of ordered, parallelism-bounded processing:

Concept Kafka Kinesis
Ordering unit Partition Shard
Throughput per unit Variable (depends on broker) 1 MB/s write, 2 MB/s read
Scale unit Partition count (add, never remove) Shard count (split/merge)
Retention Configurable (default 7 days) 24 hours (default), up to 365 days
Consumer groups Multiple, independent GetRecords (shared) or EFO (dedicated)
Partition key message.key PartitionKey (hash → shard assignment)
TYPESCRIPT
// ✅ Kinesis producer — PartitionKey determines shard assignment
import { KinesisClient, PutRecordCommand, PutRecordsCommand } from '@aws-sdk/client-kinesis'

const kinesis = new KinesisClient({ region: 'us-east-1' })

// Single record
await kinesis.send(new PutRecordCommand({
  StreamName:   'clickstream',
  PartitionKey: userId,           // same userId → same shard → ordered events per user
  Data:         Buffer.from(JSON.stringify({
    userId,
    eventType: 'page.view',
    path:      '/products/42',
    timestamp: Date.now(),
  })),
}))

// Batch: up to 500 records per PutRecords call (max 5 MB total)
const records = events.map(e => ({
  PartitionKey: e.userId,
  Data:         Buffer.from(JSON.stringify(e)),
}))

const result = await kinesis.send(new PutRecordsCommand({
  StreamName: 'clickstream',
  Records:    records,
}))

// PutRecords returns per-record success/failure — handle partial failures
const failed = result.Records?.filter(r => r.ErrorCode)
if (failed?.length) {
  // Retry only the failed records
  await retryFailedRecords(failed, records)
}

1.2 Shard Count and Capacity Planning

TYPESCRIPT
// Shard sizing formula
const writeThroughputMBps  = 50   // target: 50 MB/s ingest
const writePerShardMBps    = 1    // Kinesis limit: 1 MB/s per shard
const readConsumers        = 2    // dashboard Lambda + Flink job

// Standard GetRecords: all consumers share 2 MB/s read per shard
const readThroughputShared = 2    // MB/s per shard, shared
const requiredForReads     = Math.ceil((writeThroughputMBps * readConsumers) / readThroughputShared)

const requiredShards = Math.max(
  Math.ceil(writeThroughputMBps / writePerShardMBps),  // write constraint: 50
  requiredForReads,                                     // read constraint: 50
)

// With Enhanced Fan-Out: each consumer gets 2 MB/s dedicated — read constraint disappears
const requiredShardsEFO = Math.ceil(writeThroughputMBps / writePerShardMBps)  // 50
Performance / Safety Warning

UpdateShardCount (resharding) triggers a 24-hour cool-down period during which you cannot reshard again. Model peak throughput before provisioning — Kinesis punishes reactive scaling far more than Kafka, where you can add partitions immediately. Provision 2× your expected peak from the start.

1.3 Standard vs Enhanced Fan-Out Consumers

TYPESCRIPT
// ❌ Standard GetRecords — all consumers share 2 MB/s per shard
// At 50 shards × 2 MB/s = 100 MB/s total read bandwidth, split between all consumers
// 2 consumers → each gets 50 MB/s → may lag at high throughput

// ✅ Enhanced Fan-Out (EFO) — dedicated 2 MB/s per consumer per shard
// 2 EFO consumers × 50 shards × 2 MB/s = 200 MB/s total (no sharing)
import { RegisterStreamConsumerCommand, SubscribeToShardCommand } from '@aws-sdk/client-kinesis'

// Register this consumer as an EFO consumer (once per application)
const registration = await kinesis.send(new RegisterStreamConsumerCommand({
  StreamARN:    process.env.CLICKSTREAM_ARN!,
  ConsumerName: 'dashboard-lambda-efo',
}))

const consumerArn = registration.Consumer!.ConsumerARN!

// Subscribe to a specific shard — receives a push stream (HTTP/2 server push)
// Unlike GetRecords (polling), EFO uses long-lived connections
const response = await kinesis.send(new SubscribeToShardCommand({
  ConsumerARN: consumerArn,
  ShardId:     'shardId-000000000000',
  StartingPosition: { Type: 'LATEST' },
}))

// Process the event stream
for await (const event of response.EventStream!) {
  if (event.SubscribeToShardEvent) {
    for (const record of event.SubscribeToShardEvent.Records ?? []) {
      const data = JSON.parse(Buffer.from(record.Data!).toString())
      await processDashboardEvent(data)
    }
  }
}
Crucial Requirement

Use Enhanced Fan-Out whenever you have more than one high-throughput consumer on the same stream. Standard GetRecords splits the 2 MB/s read bandwidth across all consumers on a shard — with two consumers at 50% throughput each, lag accumulates when ingest is near the shard limit. EFO eliminates this by giving each registered consumer a dedicated 2 MB/s pipe per shard, at additional cost ($0.015/shard-hour for EFO consumers).


2. Amazon MSK

2.1 MSK Architecture Modes

Amazon MSK is managed Kafka — AWS provisions and operates the broker VMs, ZooKeeper (or KRaft), and storage. Your application uses the standard kafkajs client unchanged:

TYPESCRIPT
// ✅ kafkajs client with MSK — no code changes vs self-managed Kafka
import { Kafka } from 'kafkajs'

const kafka = new Kafka({
  clientId: 'analytics-service',
  brokers:  process.env.MSK_BOOTSTRAP_SERVERS!.split(','),
  // MSK Provisioned: 'b-1.mycluster.xxxxx.c2.kafka.us-east-1.amazonaws.com:9092'
  // MSK Serverless:  'xxxxx.c2.kafka-serverless.us-east-1.amazonaws.com:9098' (IAM auth)
  ssl: true,
  sasl: {
    mechanism: 'aws',    // MSK IAM authentication
    authenticationProvider: {
      getToken: async () => {
        // MSK IAM: token generated from AWS SDK credentials (ECS task role)
        const { generateAuthToken } = await import('aws-msk-iam-sasl-signer-js')
        return generateAuthToken({ region: 'us-east-1' })
      }
    }
  }
})
// All kafkajs APIs work identically: producers, consumers, admin, Schema Registry

2.2 MSK Provisioned vs MSK Serverless vs Self-Managed

Dimension Self-Managed Kafka MSK Provisioned MSK Serverless
Kafka API compatibility Full Full Full (some limits)
Broker management You AWS AWS
Schema Registry You run it You run it on EC2 You run it on EC2
Kafka Connect Self-host MSK Connect (managed) Not supported
ksqlDB / Kafka Streams Yes Yes Limited
Storage Self-managed EBS (provisioned IOPS) Unlimited (auto-scales)
Max throughput Hardware limit 60 Gbps per cluster Auto
Min cost EC2 cost (~$200/mo+) $0.21/broker-hr ($450/mo, 3 brokers) $0/hr + $0.10/GB processed
Cold start None None None (warm)
Best for Ops team, full control Steady high-throughput Unpredictable / dev workloads
TYPESCRIPT
// MSK Serverless: throughput auto-scales — no shard count decision
// MSK Provisioned: choose broker instance type based on throughput
//
// Broker sizing guide (MSK Provisioned):
// kafka.m5.large:  ~150 MB/s per broker, 2 vCPU, 8 GB RAM  → small workloads
// kafka.m5.xlarge: ~350 MB/s per broker, 4 vCPU, 16 GB RAM → medium workloads
// kafka.m5.2xlarge: ~700 MB/s per broker                   → high throughput
//
// Minimum cluster: 3 brokers (AZ-distributed) for production HA
// Total cluster throughput ≈ broker_throughput × broker_count / replication_factor

2.3 MSK IAM Authentication

TYPESCRIPT
// ✅ MSK IAM authentication with ECS task role — no credentials in code
// Requires: kafka-client-iam-auth-library on classpath (Java) 
// or aws-msk-iam-sasl-signer-js (Node.js)

import { generateAuthToken } from 'aws-msk-iam-sasl-signer-js'

const kafka = new Kafka({
  clientId: 'analytics-service',
  brokers:  [process.env.MSK_BOOTSTRAP!],
  ssl:      true,
  sasl: {
    mechanism: 'oauthbearer',
    oauthBearerProvider: async () => {
      const tokenProvider = await generateAuthToken({ region: 'us-east-1' })
      return {
        value: tokenProvider.token,
      }
    }
  }
})
// IAM policy required on ECS task role:
// kafka-cluster:Connect, kafka-cluster:AlterCluster, kafka-cluster:DescribeCluster
// kafka-cluster:CreateTopic, kafka-cluster:WriteData, kafka-cluster:ReadData

3. The Decision Framework


Summary

Concept Rule
Enhanced Fan-Out Kinesis Enhanced Fan-Out eliminates the shared 2 MB/s read throughput ceiling — every consumer registered for EFO gets a dedicated 2 MB/s pipe; use it whenever you have more than one high-throughput consumer on the same stream.
MSK vs Kinesis MSK is the right choice when you need Kafka API compatibility (kafkajs, Schema Registry, Kafka Connect, ksqlDB) without managing broker VMs; Kinesis is right when you want the simplest possible AWS-native integration with zero Kafka knowledge required.
Shard provisioning Kinesis shard count is the primary scale knob — but each UpdateShardCount call triggers a 24-hour cool-down; model your peak throughput before provisioning, not after.

What's Next

Part 3: The Outbox Pattern, CDC, and Exactly-Once DB-to-Broker Writes solves the hardest problem in event-driven architecture: publishing an event to a broker atomically with a database write, without a distributed transaction. The Transactional Outbox pattern and Debezium-based Change Data Capture are the two production-proven approaches.

Research & Synthesis Note

This article was developed with AI-assisted deep search, specification cross-referencing, and technical research synthesis.

#AWS#Kinesis#MSK#Managed Kafka#Event Streaming#Cloud Architecture#Backend#Distributed Systems
Siddhant Deval

Written by Siddhant Deval

Senior Full-Stack Engineer building high-scale architectures, browser performance engineering systems, and SaaS platforms.