Guide · Kinesis Lambda Event Source Mapping

Kinesis Lambda Event Source Mapping for MCP Servers — Parallelization Factor, BisectBatchOnError, and Tumbling Windows

Lambda's Kinesis Event Source Mapping (ESM) removes the consumer fleet management burden: no ECS tasks, no KCL lease tables, no worker coordination — Lambda polls each shard on your behalf and invokes your function with a batch of records. By default, ESM processes each shard serially (one invocation at a time per shard), which caps throughput at roughly the function's invocation rate. The parallelization factor setting (1–10) breaks this ceiling by allowing up to 10 concurrent Lambda invocations per shard simultaneously — a 4-shard stream with parallelization factor 3 can run 12 concurrent Lambda invocations. For an MCP probe pipeline with bursty ingest during a scan wave, parallelization factor prevents the iterator age from growing while keeping the operational surface to zero worker management.

TL;DR

Set parallelizationFactor: 2-4 for probe result consumers that need to keep up with scan wave bursts without managing a consumer fleet. Enable bisectBatchOnFunctionError: true to isolate poison-pill records — on error, ESM splits the batch in half recursively until it finds and quarantines the failing record. Use maximumBatchingWindowInSeconds: 5-30 to accumulate larger batches for cost efficiency (reduces invocation count without adding meaningful latency). Configure an onFailure destination to an SQS queue to capture records that exhaust retries. Enable Enhanced Fan-Out on the ESM for <70ms latency vs ~200ms standard polling. Monitor GetRecords.IteratorAgeMilliseconds and alarm on any shard exceeding 60,000ms.

Parallelization factor: concurrent invocations per shard

Without parallelization, one Lambda invocation processes one batch from one shard at a time — the next invocation for that shard starts only after the current one finishes. With parallelizationFactor: N, ESM maintains up to N in-flight invocations per shard simultaneously, processing N non-overlapping record windows in parallel:

parallelizationFactorShardsMax concurrent Lambda invocationsUse case
1 (default)44Low-throughput probe result processing
248Moderate bursts during scan waves
5420High-throughput pipelines with slow downstream sinks
10 (max)440Maximum throughput; verify Lambda concurrency limits apply

Parallelization factor does not change shard ordering guarantees within a single shard: records from the same shard are still processed in partition-key order within each parallel window. Records in different parallel windows may be processed out of order relative to each other. For probe results, this is typically acceptable — probe result upserts are commutative (last write wins by probeRunAt timestamp). If your consumer requires strict FIFO ordering across all records in a shard, keep parallelizationFactor: 1.

import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as kinesis from 'aws-cdk-lib/aws-kinesis';
import * as eventsources from 'aws-cdk-lib/aws-lambda-event-sources';
import * as sqs from 'aws-cdk-lib/aws-sqs';

const probeResultStream = kinesis.Stream.fromStreamArn(this, 'ProbeStream',
  'arn:aws:kinesis:us-east-1:123456789012:stream/alivemcp-probe-results'
);
const dlq = new sqs.Queue(this, 'ProbeResultDlq', {
  queueName: 'alivemcp-probe-result-dlq',
  retentionPeriod: cdk.Duration.days(14),
});

probeConsumer.addEventSource(new eventsources.KinesisEventSource(probeResultStream, {
  startingPosition: lambda.StartingPosition.LATEST,
  batchSize: 100,
  parallelizationFactor: 3,
  bisectBatchOnError: true,
  maxBatchingWindow: cdk.Duration.seconds(10),
  retryAttempts: 3,
  onFailure: new eventsources.SqsDlq(dlq),
}));

bisectBatchOnFunctionError: isolating poison-pill records

Without bisectBatchOnFunctionError, a single malformed probe result record in a batch of 100 causes the entire batch to retry until it exhausts retryAttempts and the records age out or hit maximumRecordAgeInSeconds. The healthy 99 records in the batch are blocked behind the one bad record for minutes.

With bisectBatchOnFunctionError: true, ESM responds to a function error by splitting the failing batch in half and retrying each half independently. The half containing the bad record fails again; ESM splits it again. This binary search continues until the batch size is 1 — the single failing record is then routed to the onFailure destination (SQS DLQ) and processing resumes for the healthy records.

# Bisect behavior on a batch of 8 records where record 5 is malformed:
Attempt 1: batch [1,2,3,4,5,6,7,8] → function throws → bisect
Attempt 2a: batch [1,2,3,4] → success ✓
Attempt 2b: batch [5,6,7,8] → function throws → bisect
Attempt 3a: batch [5,6] → function throws → bisect
Attempt 3b: batch [7,8] → success ✓
Attempt 4a: batch [5] → function throws → onFailure DLQ → poison pill quarantined
Attempt 4b: batch [6] → success ✓

Total successful processing: records 1,2,3,4,6,7,8 ✓
Quarantined: record 5 → DLQ for investigation

Enable bisect for any consumer where the batch may contain independently-processable records (probe results from different endpoints). Do not rely on bisect as the primary error handling strategy — fix the root cause. Bisect is a safety net that prevents one bad record from blocking the entire shard indefinitely.

Batch window and batch size: cost vs latency

maximumBatchingWindowInSeconds (0–300 seconds) controls how long ESM waits before invoking Lambda even if fewer than batchSize records are available. At 0 (default), ESM invokes Lambda as soon as any records are available — maximizing freshness but potentially invoking Lambda with very small batches during low-traffic periods.

ScenariobatchSizebatchWindowEffect
Real-time alerter (status page)100sImmediate invocation; minimal latency; higher invocation cost during low traffic
Probe result archiver50030sWaits up to 30s to fill a larger batch; fewer, cheaper invocations
Analytics aggregator100060sLarge batches every minute; efficient for batch writes to DynamoDB
Balanced consumer1005sGood default: 5s freshness, batches of up to 100

The probe result archiver that writes to S3 benefits most from a large batch window: each Lambda invocation opens an S3 PutObject, and consolidating 500 records into one S3 object costs the same as 500 separate objects at 1/500th of the request cost. For the real-time alerter that drives the status page UI, any batch window adds noticeable latency — keep it at 0.

Tumbling window aggregations

ESM supports tumbling windows for computing aggregations over a time-bounded window of records without needing an external state store. Specify tumblingWindow as a duration (15 seconds to 15 minutes); ESM provides a state object in the event that persists between invocations within the same window:

export const handler = async (event: KinesisStreamEvent): Promise => {
  // event.state: accumulated from prior invocations in this window
  // event.isFinalInvokeForWindow: true on last call before window closes
  // event.isWindowTerminatedEarly: true if window ended early (batch too large)

  const state = (event.state as { counters?: Record }) ?? { counters: {} };
  const counters = state.counters ?? {};

  for (const record of event.Records) {
    const probe = JSON.parse(Buffer.from(record.kinesis.data, 'base64').toString());
    counters[probe.endpointId] = (counters[probe.endpointId] ?? 0) + 1;
  }

  if (event.isFinalInvokeForWindow) {
    // Window closed — flush aggregated counters to DynamoDB
    await writeWindowCounters(counters, event.window);
    return { state: {}, batchItemFailures: [] };
  }

  // Return updated state for next invocation in this window
  return { state: { counters }, batchItemFailures: [] };
};

Tumbling windows are ideal for computing per-endpoint probe counts per minute (for rate-of-change alerts) or uptime percentages within a rolling window. Without tumbling windows, you would need DynamoDB atomic counters or ElastiCache to accumulate partial results between Lambda invocations — tumbling windows provide this state internally at no extra infrastructure cost.

Enhanced Fan-Out mode for Lambda ESM

By default, Lambda ESM uses standard GetRecords polling (shared 2 MiB/s per shard). Enable EFO mode on the ESM by setting startingPosition: LATEST and adding the consumerArn parameter pointing to a registered EFO consumer:

# CDK: enable EFO on Lambda ESM
probeConsumer.addEventSource(new eventsources.KinesisEventSource(probeResultStream, {
  startingPosition: lambda.StartingPosition.LATEST,
  // EFO: dedicated 2 MiB/s regardless of other consumers on the stream
  // Register consumer first via RegisterStreamConsumer, then pass ARN here
  // Note: CDK does not yet have native EFO support — use CfnEventSourceMapping directly
}));

// CfnEventSourceMapping for EFO
new lambda.CfnEventSourceMapping(this, 'ProbeResultEsmEfo', {
  functionName: probeConsumer.functionName,
  eventSourceArn: probeResultStream.streamArn,
  startingPosition: 'LATEST',
  parallelizationFactor: 3,
  bisectBatchOnFunctionError: true,
  selfManagedEventSource: undefined,
  amazonManagedKafkaEventSourceConfig: undefined,
  // EFO via consumer ARN in FilterCriteria is not available yet — use standard ESM
  // and RegisterStreamConsumer separately for EFO control
});

As of 2026, Lambda ESM supports EFO via the DestinationConfig and consumer ARN fields in CreateEventSourceMapping. Check the current SDK documentation for the exact parameter name — the CDK L2 construct may not expose it yet; use the L1 CfnEventSourceMapping when needed.

CloudWatch monitoring for Kinesis ESM

MetricNamespaceAlarm threshold
GetRecords.IteratorAgeMillisecondsAWS/Kinesis> 60,000ms — Lambda is behind the probe write rate by more than 1 probe cycle
IteratorAgeAWS/Lambda (ESM)Alternative view — age of oldest record in current Lambda invocation batch
ThrottlesAWS/Lambda> 0 sustained — Lambda concurrency limit reached; increase reserved concurrency
ErrorsAWS/Lambda> 0 — bisect will kick in but investigate root cause
ApproximateNumberOfMessagesVisibleAWS/SQS (DLQ)> 0 — records failed after all retries; investigate DLQ messages

The most important metric for ESM consumers is GetRecords.IteratorAgeMilliseconds — the age of the oldest unread record in the shard. A growing iterator age means Lambda is processing records slower than Kinesis is receiving them. First check for Lambda throttles (concurrency limit); then check for slow processing (increase parallelizationFactor or optimize the function); then check for Kinesis shard throughput limits (consider adding shards).

Common failure modes

SymptomCauseFix
IteratorAge growing continuouslyLambda too slow or throttledIncrease parallelizationFactor; check Lambda concurrency limit; optimize processRecords
Records lost (not in DLQ either)maximumRecordAgeInSeconds expired before retries exhaustedLower retryAttempts or increase maximumRecordAgeInSeconds (max 7 days); add onFailure DLQ
DLQ filling rapidly with same recordMalformed record that bisect isolates but function always rejectsAdd explicit deserialization error handling that checkpoints past bad records; fix producer
Lambda invoked with 1-record batches unexpectedlybisectBatchOnFunctionError recursing to batch size=1 oftenFix underlying processing error; check CloudWatch Lambda Errors for the root cause
Tumbling window state lost mid-windowLambda function timeout fired before window closedIncrease function timeout to be > tumblingWindow duration; return partial state on timeout
ESM paused after consecutive errorsLambda error rate exceeded; ESM backed off exponentially and pausedFix Lambda errors; ESM resumes automatically after 30min or on ESM update