Guide · DynamoDB Streams Advanced
DynamoDB Streams Lambda Filter Criteria for MCP Servers — Event Type Filtering, Attribute Patterns
Lambda ESM FilterCriteria lets you declare which DynamoDB Streams records invoke your function — without changing a line of application code. Before filter criteria existed, every record in every DynamoDB Streams shard invoked the Lambda function regardless of relevance; the function had to inspect the event type and attributes before deciding whether to act. For an AliveMCP probe state table, this meant a Lambda alerter would receive and discard every routine MODIFY event (probe passed, no change in status) before finding the rare INSERT or MODIFY with status = "DEGRADED" that should trigger a PagerDuty notification. With filter criteria, Lambda evaluates the pattern before invoking your function — filtered records are still read from the shard and counted toward the shard's read throughput, but they never cause a function invocation, which eliminates compute cost and reduces alert latency by removing no-op invocations from the critical path.
TL;DR
Attach a FilterCriteria object to your Lambda ESM to scope DynamoDB Streams invocations to only the records you care about. For an MCP probe state table, set eventName = INSERT to process only new endpoint registrations, or combine eventName = MODIFY with dynamodb.NewImage.status.S = "DEGRADED" to fire the alerter only on status degradations. Up to 5 filters per ESM (OR-combined); within one filter, all patterns are AND-combined. Filtered records still consume shard read capacity — filtering reduces Lambda invocations and cost, not DynamoDB read throughput. CDK exposes this via DynamoEventSource({ filters: [...] }).
Filter criteria structure and evaluation model
Lambda evaluates filter criteria before invoking the function. Each filter is a JSON object that specifies patterns against the DynamoDB Streams record structure. Multiple filters in the same FilterCriteria are OR-combined: a record matches if it satisfies any one filter. Within a single filter, all patterns are AND-combined: every specified pattern must match for the filter to pass.
The DynamoDB Streams record structure that filters act on:
{
"eventName": "MODIFY", // INSERT | MODIFY | REMOVE
"dynamodb": {
"Keys": {
"PK": { "S": "SERVER#alivemcp.com" },
"SK": { "S": "STATUS" }
},
"NewImage": {
"PK": { "S": "SERVER#alivemcp.com" },
"SK": { "S": "STATUS" },
"status": { "S": "DEGRADED" },
"consecutiveFailures": { "N": "5" },
"lastCheckedAt": { "N": "1735600000" }
},
"OldImage": {
"status": { "S": "HEALTHY" },
"consecutiveFailures": { "N": "0" }
},
"StreamViewType": "NEW_AND_OLD_IMAGES"
}
}
Filter patterns use the same JSON path syntax as EventBridge rules. The top-level keys (eventName, dynamodb) map directly to the DynamoDB Streams record. Attribute access uses dot notation; DynamoDB type descriptors (S, N, BOOL, L, M) are part of the path.
Filter pattern types
Lambda ESM filter criteria support a subset of the EventBridge content filtering patterns:
| Pattern type | Syntax | Example use case |
|---|---|---|
| Exact match (string) | { "eventName": ["INSERT"] } | Only process new endpoint registrations |
| Exact match (multiple values = OR) | { "eventName": ["INSERT", "MODIFY"] } | Process inserts and updates, skip deletes |
| Prefix match | { "dynamodb": { "Keys": { "PK": { "S": [{ "prefix": "SERVER#" }] } } } } | Only process records for server-type partition keys |
| Numeric comparison | { "dynamodb": { "NewImage": { "consecutiveFailures": { "N": [{ "numeric": [">", 2] }] } } } } | Alert only after 3+ consecutive failures |
| Exists / not exists | { "dynamodb": { "NewImage": { "alertSent": [{ "exists": false }] } } } | Only process records where alert has not yet been sent |
| Anything-but | { "dynamodb": { "NewImage": { "status": { "S": [{ "anything-but": ["HEALTHY"] }] } } } } | Fire on any status except HEALTHY |
Numeric comparisons operate on the DynamoDB N type value as a number, not a string — so "N": [{"numeric": [">", 2]}] correctly compares the attribute value numerically even though DynamoDB stores numbers as strings. Supported operators: =, !=, <, <=, >, >=, and range ([">", 0, "<=", 100]).
Practical patterns for MCP probe state tables
An AliveMCP probe state table has a natural set of filtering use cases: alerting on status degradation, processing only new endpoint registrations, and reacting to recovery events. Each maps to a distinct filter pattern:
Pattern 1: Status degradation alerter — trigger Lambda only when a probe transitions from HEALTHY to DEGRADED or DOWN:
// AWS SDK / CloudFormation / CDK filter for degradation alerting
const degradationFilter = {
Pattern: JSON.stringify({
eventName: ["MODIFY"],
dynamodb: {
NewImage: {
status: { S: [{ "anything-but": ["HEALTHY"] }] }
},
OldImage: {
status: { S: ["HEALTHY"] } // Only fire on transitions FROM healthy
}
}
})
};
Pattern 2: New endpoint registration — trigger downstream provisioning (create monitoring schedule, initialize metrics) only on first INSERT of a server record:
const registrationFilter = {
Pattern: JSON.stringify({
eventName: ["INSERT"],
dynamodb: {
Keys: {
PK: { S: [{ prefix: "SERVER#" }] },
SK: { S: ["METADATA"] } // Only the canonical metadata record, not status sub-records
}
}
})
};
Pattern 3: Recovery events — log recovery metrics when a server transitions back to HEALTHY after a degraded period:
const recoveryFilter = {
Pattern: JSON.stringify({
eventName: ["MODIFY"],
dynamodb: {
NewImage: { status: { S: ["HEALTHY"] } },
OldImage: { status: { S: [{ "anything-but": ["HEALTHY"] }] } }
}
})
};
These three filters can be combined into a single ESM with all three as separate filter objects (OR-combined), using up to 5 filters per ESM.
CDK DynamoEventSource with FilterCriteria
In AWS CDK (TypeScript), attach filters via the DynamoEventSource construct. Lambda ESM supports up to 5 filter patterns per event source mapping:
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as eventsources from 'aws-cdk-lib/aws-lambda-event-sources';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
declare const probeTable: dynamodb.Table;
declare const alerterFn: lambda.Function;
// Alerter: only invoked on status degradation transitions
alerterFn.addEventSource(new eventsources.DynamoEventSource(probeTable, {
startingPosition: lambda.StartingPosition.LATEST,
batchSize: 100,
maxBatchingWindow: cdk.Duration.seconds(5), // Wait up to 5s to batch degradation events
bisectBatchOnError: true, // Isolate poison-pill records via binary split
retryAttempts: 2,
onFailure: new eventsources.SqsDlq(dlqQueue),
streamViewType: dynamodb.StreamViewType.NEW_AND_OLD_IMAGES, // Required for OldImage filtering
filters: [
// Filter 1: status degradation (HEALTHY → DEGRADED or DOWN)
lambda.FilterCriteria.filter({
eventName: lambda.FilterRule.isEqual('MODIFY'),
dynamodb: {
NewImage: { status: { S: lambda.FilterRule.notExists() } }
}
}),
// Filter 2: status degradation (explicit DEGRADED)
lambda.FilterCriteria.filter({
eventName: lambda.FilterRule.isEqual('MODIFY'),
dynamodb: {
NewImage: { status: { S: lambda.FilterRule.isEqual('DEGRADED') } },
OldImage: { status: { S: lambda.FilterRule.isEqual('HEALTHY') } }
}
}),
// Filter 3: new server registration
lambda.FilterCriteria.filter({
eventName: lambda.FilterRule.isEqual('INSERT'),
dynamodb: {
Keys: { SK: { S: lambda.FilterRule.isEqual('METADATA') } }
}
})
]
}));
The CDK FilterRule helper maps to the underlying JSON filter pattern syntax. Note that streamViewType must be NEW_AND_OLD_IMAGES if your filters reference OldImage — if the stream is configured with NEW_IMAGE only, OldImage will be absent from all records and OldImage-based filters will never match.
Performance and cost implications
A common misconception is that filter criteria reduce the DynamoDB Streams read throughput consumed by Lambda ESM. They do not. Lambda ESM reads every record from every shard regardless of filter criteria — filtering happens in the Lambda service layer after reading, before deciding whether to invoke the function. The performance benefits are:
| Metric | Without filters | With filters (95% filtered) |
|---|---|---|
| DynamoDB Streams read throughput | 100% of records read | 100% of records read (no change) |
| Lambda invocations | Every batch invokes Lambda | Only batches with matching records invoke Lambda |
| Lambda compute cost | Paid for every invocation | ~95% reduction if 95% of records are filtered |
| Alert latency | Higher — no-op invocations add queue depth | Lower — matching batches processed without competition from no-ops |
| Lambda concurrency | One concurrent execution per shard | One concurrent execution per shard (filtered shards release concurrency sooner) |
For a probe state table with 1,000 monitored servers and 60-second probe intervals, roughly 1,000 MODIFY events per minute are written for routine HEALTHY probe results. If only 1% of probes find degraded endpoints, filter criteria reduce Lambda alerter invocations from ~17/second to ~0.17/second — a 100× compute cost reduction. The DynamoDB Streams shard read capacity consumed remains the same.
StreamViewType requirements for filtering
Filter criteria access the record structure as written to the stream. The stream's StreamViewType determines which images are available for filtering:
| StreamViewType | Available for filtering | Limitation |
|---|---|---|
| NEW_IMAGE | eventName, dynamodb.NewImage, dynamodb.Keys | Cannot filter on OldImage — transition filters impossible |
| OLD_IMAGE | eventName, dynamodb.OldImage, dynamodb.Keys | Cannot filter on NewImage — cannot react to new attribute values |
| NEW_AND_OLD_IMAGES | eventName, dynamodb.NewImage, dynamodb.OldImage, dynamodb.Keys | Highest storage cost; required for transition-based filters |
| KEYS_ONLY | eventName, dynamodb.Keys only | Only event type and partition/sort key available — no attribute filtering |
For AliveMCP probe state alerting, NEW_AND_OLD_IMAGES is required because the most critical filter — detecting status transitions from HEALTHY to DEGRADED — requires comparing OldImage.status against NewImage.status. StreamViewType cannot be changed on an existing stream; you must disable and re-enable streams on the table (which resets the stream's shard sequence) or create a new table.
Complete failure mode reference
| Symptom | Root cause | Detection | Fix |
|---|---|---|---|
| Alerts not triggering despite status degradation | Filter pattern references OldImage but StreamViewType is NEW_IMAGE — OldImage absent from all records | Lambda CloudWatch logs show zero invocations during known degradation events; filter match rate is 0% | Change table StreamViewType to NEW_AND_OLD_IMAGES; note stream shard reset when changing view type |
| Filter never matches REMOVE events for deleted servers | REMOVE records in DynamoDB Streams have Keys and OldImage but no NewImage — NewImage-based filter patterns never match REMOVE | Verified REMOVE events in stream but Lambda never invoked for server deletions | Add separate filter with eventName = REMOVE and no NewImage pattern; use OldImage for REMOVE attribute matching |
| Numeric filter not matching despite correct attribute value | Attribute stored as DynamoDB String type (S) not Number type (N) — numeric comparison only works against N-typed attributes | No invocations despite records containing the target numeric value; confirm attribute type in DynamoDB console | Change attribute type to N in DynamoDB schema; or use string comparison for attributes stored as S even if they represent numbers |
| Filter criteria ignored — Lambda invoked for all records | More than 5 filters specified (limit exceeded); or filter JSON syntax malformed — Lambda ESM falls back to no filtering | Lambda invocation count equals total stream record count; CloudWatch ESM filter metrics show 0 filtered records | Reduce to ≤5 filters per ESM; validate filter JSON against EventBridge content filter syntax; use AWS CLI get-event-source-mapping to inspect active filter configuration |
| Alert fired for same degradation event multiple times | bisectBatchOnError splits batch on Lambda error; if filter passes a batch containing both good and bad records, bisect may re-present the matching record multiple times | PagerDuty shows duplicate alert IDs within seconds of each other | Make Lambda alerter idempotent by checking alert state in DynamoDB before firing; use deduplication window in PagerDuty or OpsGenie |
| OldImage filter matches on INSERT events unexpectedly | INSERT records have no OldImage; a filter pattern with OldImage patterns is evaluated against absent OldImage as if OldImage keys don't exist — exists:false patterns will match | Lambda invoked for INSERT events when filter was intended to only match MODIFY transitions | Always include explicit eventName filter for INSERT/MODIFY/REMOVE to prevent cross-event-type matches; don't rely on OldImage absence as an implicit INSERT discriminator |