Guide · AWS Lambda

MCP Server Lambda Event Source Mapping Filters — FilterCriteria, SQS, Kinesis, DynamoDB Streams

Lambda EventSourceMapping filters let you declare which records from SQS, Kinesis, or DynamoDB Streams should trigger your function — records that do not match are discarded at the source, never invoking Lambda. Without filters, every record in a high-throughput Kinesis stream or DynamoDB Streams shard triggers Lambda, even if 95% of those records are irrelevant to the function's logic. Filtering moves the relevance check out of your handler code and into the Lambda service layer: filtered-out records are not counted as Lambda invocations, so you pay nothing for them. For MCP server workloads, common uses are: processing only ERROR-level events from a Kinesis call-log stream, acting only on INSERT operations in a DynamoDB probe-results table, or routing SQS messages by toolName field to specialised handler functions.

TL;DR

Set FilterCriteria on the EventSourceMapping. For SQS, filter on body (parsed as JSON). For Kinesis, filter on data (decoded from base64). For DynamoDB Streams, filter on dynamodb.NewImage, dynamodb.OldImage, or eventName (INSERT / MODIFY / REMOVE). Multiple patterns use OR semantics — any matching pattern triggers the Lambda. Fields within a single pattern use AND semantics. Filtered-out records are acknowledged and discarded — they do not block the shard/partition.

Filter pattern syntax

Filter patterns are JSON objects where each key is a path in the event record and each value is an array of match conditions. A record matches a pattern if all field conditions in that pattern are satisfied (AND). A record is processed if it matches any pattern (OR).

Condition typeSyntaxExample
Exact string match"value"{"body": {"level": ["ERROR"]}}
String prefix{"prefix": "..."}{"body": {"endpoint": [{"prefix": "https://"}]}}
String suffix{"suffix": "..."}{"body": {"path": [{"suffix": "/mcp"}]}}
Anything-but{"anything-but": ["v1", "v2"]}{"body": {"version": [{"anything-but": ["1.0"]}]}}
Numeric range{"numeric": [">", 100, "<=", 500]}{"body": {"latencyMs": [{"numeric": [">", 1000]}]}}
Exists / not-exists{"exists": true}{"body": {"errorCode": [{"exists": true}]}}
Nullnull{"body": {"schemaHash": [null]}}

Conditions are applied after automatic JSON parsing: SQS body is parsed from a string if it is valid JSON; Kinesis data is decoded from base64 then parsed as JSON if valid. If the body is not valid JSON, only string-level conditions (exact, prefix, suffix) on the raw string apply.

SQS filter examples for MCP probe results

# Create an ESM with filter: only process messages where level == "ERROR"
# and where endpoint starts with "https://"
aws lambda create-event-source-mapping \
  --function-name ProbeAlertHandler \
  --event-source-arn arn:aws:sqs:us-east-1:123456789012:probe-results \
  --batch-size 10 \
  --filter-criteria '{
    "Filters": [
      {
        "Pattern": "{\"body\":{\"level\":[\"ERROR\"],\"endpoint\":[{\"prefix\":\"https://\"}]}}"
      },
      {
        "Pattern": "{\"body\":{\"level\":[\"DEGRADED\"],\"latencyMs\":[{\"numeric\":[\">\",5000]}]}}"
      }
    ]
  }'

This example has two filter patterns with OR semantics: Lambda is invoked if a message has level == "ERROR" AND endpoint starts with https://, OR if the message has level == "DEGRADED" AND latencyMs > 5000. An INFO-level message with any latency is discarded.

DynamoDB Streams filter: act only on INSERT or specific item types

// CDK: EventSourceMapping with DynamoDB Streams filters
import * as lambda from "aws-cdk-lib/aws-lambda";
import * as dynamodb from "aws-cdk-lib/aws-dynamodb";

const probeResultsTable = new dynamodb.Table(this, "ProbeResults", {
  partitionKey: { name: "pk", type: dynamodb.AttributeType.STRING },
  sortKey: { name: "sk", type: dynamodb.AttributeType.STRING },
  stream: dynamodb.StreamViewType.NEW_AND_OLD_IMAGES,  // required for NewImage filters
});

const analyticsWorker = new lambda.Function(this, "AnalyticsWorker", {
  // ...
});

// Filter: only INSERT events where item type is "probe_result"
// This skips MODIFY (status updates) and REMOVE (TTL expiry cleanup)
new lambda.EventSourceMapping(this, "ProbeResultStream", {
  target: analyticsWorker,
  eventSourceArn: probeResultsTable.tableStreamArn,
  startingPosition: lambda.StartingPosition.LATEST,
  batchSize: 50,
  bisectBatchOnError: true,
  filters: [
    lambda.FilterCriteria.filter({
      eventName: lambda.FilterRule.isEqual("INSERT"),
      dynamodb: {
        NewImage: {
          itemType: { S: lambda.FilterRule.isEqual("probe_result") },
        },
      },
    }),
  ],
});

The dynamodb.NewImage path uses DynamoDB's typed attribute format: { "S": "value" } for strings, { "N": "42" } for numbers (numbers are strings in the DynamoDB JSON wire format). A filter on NewImage requires the EventSourceMapping to use NEW_AND_OLD_IMAGES or NEW_IMAGE stream view type — KEYS_ONLY does not include image data and the filter will never match.

DynamoDB REMOVE for TTL expiry filtering

// Only process TTL-expired items (not manual deletes)
// TTL expiry events have userIdentity.type == "Service" and
// userIdentity.principalId == "dynamodb.amazonaws.com"
lambda.FilterCriteria.filter({
  eventName: lambda.FilterRule.isEqual("REMOVE"),
  userIdentity: {
    type: lambda.FilterRule.isEqual("Service"),
    principalId: lambda.FilterRule.isEqual("dynamodb.amazonaws.com"),
  },
})

Kinesis filter: only process specific event types from a shared stream

// Kinesis ESM: only invoke Lambda for records where data.eventType == "probe_failure"
// Kinesis data field is base64-encoded; Lambda decodes it and parses JSON
new lambda.EventSourceMapping(this, "ProbeLogStream", {
  target: alertHandlerFn,
  eventSourceArn: probeLogStream.streamArn,
  startingPosition: lambda.StartingPosition.TRIM_HORIZON,
  batchSize: 100,
  bisectBatchOnError: true,
  filters: [
    lambda.FilterCriteria.filter({
      data: {
        eventType: lambda.FilterRule.isEqual("probe_failure"),
      },
    }),
    lambda.FilterCriteria.filter({
      data: {
        eventType: lambda.FilterRule.isEqual("schema_drift"),
      },
    }),
  ],
});

With these two filters, the Kinesis ESM triggers the alert handler only when a record's decoded JSON contains eventType == "probe_failure" OR eventType == "schema_drift". eventType == "probe_success" records flow through the Kinesis shard without invoking Lambda — the shard's iterator advances past them as usual (filters do not pause shard processing).

Cost and throughput impact

MetricWithout filterWith filter (90% filtered out)
Lambda invocations billed100% of records (batched)10% of records — 90% discarded at source
Lambda GB-seconds billedProportional to all recordsProportional to matching records only
Kinesis shard iterator progressAdvances after every batch processedAdvances after every batch regardless of filters — filtered records do not hold up the iterator
DynamoDB Streams shardAll change events trigger LambdaOnly matching events trigger Lambda; shard reads proceed normally
SQS batchAll messages in batch invoke LambdaLambda invoked only if at least one message in the batch matches; non-matching messages are deleted from SQS automatically

One nuance for SQS: the filter applies per-message within a batch. If a batch of 10 messages has 2 matching and 8 non-matching, Lambda is invoked with only the 2 matching messages. The 8 non-matching messages are deleted from the queue automatically — they do not go to the DLQ. This is correct behavior (they were intentionally excluded), but may surprise teams that monitor DLQ depth to catch non-matching messages.

Common failure modes

SymptomCauseFix
No Lambda invocations despite records in streamAll records filtered out; filter pattern is too restrictive or path is wrongTest filter pattern against sample records using aws lambda test-event-source-mapping; check JSON path matches exact wire format
DynamoDB filter never matches despite correct field valuesFiltering on NewImage.field.S but stream is KEYS_ONLY — no NewImage in stream recordsChange table stream to NEW_IMAGE or NEW_AND_OLD_IMAGES
SQS non-matching messages silently deleted (not in DLQ)Filter discards non-matching messages — they are acknowledged and deleted from SQS, not reroutedThis is correct behavior. If you need a DLQ for non-matching messages, use a separate queue and a different routing mechanism (SQS message filtering via subscription filter policies)
Kinesis filter syntax error causes ESM to stop processingInvalid filter pattern JSON (malformed numeric condition, invalid path)Validate filter JSON before applying; check ESM state via aws lambda get-event-source-mapping for State: Disabled or error message
Filter works in test but not productionProduction SQS messages have body as a stringified JSON double-encoded (e.g., SNS-to-SQS envelope wraps the real payload)Adjust filter path to match the outer envelope structure: {"body": {"Message": [{"prefix": "..."}]}} for SNS-wrapped messages