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:
| parallelizationFactor | Shards | Max concurrent Lambda invocations | Use case |
|---|---|---|---|
| 1 (default) | 4 | 4 | Low-throughput probe result processing |
| 2 | 4 | 8 | Moderate bursts during scan waves |
| 5 | 4 | 20 | High-throughput pipelines with slow downstream sinks |
| 10 (max) | 4 | 40 | Maximum 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.
| Scenario | batchSize | batchWindow | Effect |
|---|---|---|---|
| Real-time alerter (status page) | 10 | 0s | Immediate invocation; minimal latency; higher invocation cost during low traffic |
| Probe result archiver | 500 | 30s | Waits up to 30s to fill a larger batch; fewer, cheaper invocations |
| Analytics aggregator | 1000 | 60s | Large batches every minute; efficient for batch writes to DynamoDB |
| Balanced consumer | 100 | 5s | Good 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
| Metric | Namespace | Alarm threshold |
|---|---|---|
GetRecords.IteratorAgeMilliseconds | AWS/Kinesis | > 60,000ms — Lambda is behind the probe write rate by more than 1 probe cycle |
IteratorAge | AWS/Lambda (ESM) | Alternative view — age of oldest record in current Lambda invocation batch |
Throttles | AWS/Lambda | > 0 sustained — Lambda concurrency limit reached; increase reserved concurrency |
Errors | AWS/Lambda | > 0 — bisect will kick in but investigate root cause |
ApproximateNumberOfMessagesVisible | AWS/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
| Symptom | Cause | Fix |
|---|---|---|
| IteratorAge growing continuously | Lambda too slow or throttled | Increase parallelizationFactor; check Lambda concurrency limit; optimize processRecords |
| Records lost (not in DLQ either) | maximumRecordAgeInSeconds expired before retries exhausted | Lower retryAttempts or increase maximumRecordAgeInSeconds (max 7 days); add onFailure DLQ |
| DLQ filling rapidly with same record | Malformed record that bisect isolates but function always rejects | Add explicit deserialization error handling that checkpoints past bad records; fix producer |
| Lambda invoked with 1-record batches unexpectedly | bisectBatchOnFunctionError recursing to batch size=1 often | Fix underlying processing error; check CloudWatch Lambda Errors for the root cause |
| Tumbling window state lost mid-window | Lambda function timeout fired before window closed | Increase function timeout to be > tumblingWindow duration; return partial state on timeout |
| ESM paused after consecutive errors | Lambda error rate exceeded; ESM backed off exponentially and paused | Fix Lambda errors; ESM resumes automatically after 30min or on ESM update |