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 typeSyntaxExample 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:

MetricWithout filtersWith filters (95% filtered)
DynamoDB Streams read throughput100% of records read100% of records read (no change)
Lambda invocationsEvery batch invokes LambdaOnly batches with matching records invoke Lambda
Lambda compute costPaid for every invocation~95% reduction if 95% of records are filtered
Alert latencyHigher — no-op invocations add queue depthLower — matching batches processed without competition from no-ops
Lambda concurrencyOne concurrent execution per shardOne 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:

StreamViewTypeAvailable for filteringLimitation
NEW_IMAGEeventName, dynamodb.NewImage, dynamodb.KeysCannot filter on OldImage — transition filters impossible
OLD_IMAGEeventName, dynamodb.OldImage, dynamodb.KeysCannot filter on NewImage — cannot react to new attribute values
NEW_AND_OLD_IMAGESeventName, dynamodb.NewImage, dynamodb.OldImage, dynamodb.KeysHighest storage cost; required for transition-based filters
KEYS_ONLYeventName, dynamodb.Keys onlyOnly 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

SymptomRoot causeDetectionFix
Alerts not triggering despite status degradationFilter pattern references OldImage but StreamViewType is NEW_IMAGE — OldImage absent from all recordsLambda 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 serversREMOVE records in DynamoDB Streams have Keys and OldImage but no NewImage — NewImage-based filter patterns never match REMOVEVerified REMOVE events in stream but Lambda never invoked for server deletionsAdd separate filter with eventName = REMOVE and no NewImage pattern; use OldImage for REMOVE attribute matching
Numeric filter not matching despite correct attribute valueAttribute stored as DynamoDB String type (S) not Number type (N) — numeric comparison only works against N-typed attributesNo invocations despite records containing the target numeric value; confirm attribute type in DynamoDB consoleChange 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 recordsMore than 5 filters specified (limit exceeded); or filter JSON syntax malformed — Lambda ESM falls back to no filteringLambda invocation count equals total stream record count; CloudWatch ESM filter metrics show 0 filtered recordsReduce 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 timesbisectBatchOnError splits batch on Lambda error; if filter passes a batch containing both good and bad records, bisect may re-present the matching record multiple timesPagerDuty shows duplicate alert IDs within seconds of each otherMake Lambda alerter idempotent by checking alert state in DynamoDB before firing; use deduplication window in PagerDuty or OpsGenie
OldImage filter matches on INSERT events unexpectedlyINSERT 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 matchLambda invoked for INSERT events when filter was intended to only match MODIFY transitionsAlways 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