7 views
EventBridge Pipes look like free glue: point SQS or Kinesis at Lambda, drop a filter, maybe an enrichment step, and delete the custom “router” Lambda that only existed to fan messages. The unfair advantage of EventBridge Pipes to Lambda is not “use Pipes” — it is treating filters and enrichment as a fail-closed path. A wrong pattern, a 403 on enrichment, or an enrichment timeout that still marks the source record consumed is how you lose orders without a single red CloudWatch alarm on the target.
⚡ TL;DR: Design EventBridge Pipes to Lambda as filter → enrich → target with explicit drop vs DLQ semantics. Prefer source-native filters that fail closed (unknown fields do not match). Enrichment must be idempotent, IAM-scoped, and timeout-bounded; never mutate identity keys. On Lambda targets, enable partial-batch failure reporting and wire source DLQs / on-failure destinations. Prove every discard path with metrics before cutting over from a custom router.For agent workloads on the same bus, see Bedrock Agents + EventBridge, EventBridge Scheduler agent batches, and Lambda destinations for failed tools.
EventBridge Pipes to Lambda: model the pipe as a contract
A Pipe has four stages that each can drop or fail: source poll, filter, enrichment, target. Teams that only test the happy path discover in prod that enrichment failures can still advance the source cursor depending on configuration. Write the contract down before CDK.typescript
If product later adds // contracts/order-pipe.ts — explicit semantics for ops and reviewers
export type PipeContract = {
source: "sqs" | "kinesis" | "dynamodb-stream" | "kafka";
filter: {
// ✅ Match only known event types; unknown → drop (fail closed for enrichment)
matchEventTypes: string[];
onUnknownField: "drop" | "dlq"; // never "forward_anyway"
};
enrichment?: {
type: "lambda" | "api-destination" | "step-functions";
timeoutMs: number;
onError: "retry_then_dlq" | "dlq" | "fail_pipe"; // ❌ never silent continue
idempotencyKey: "sourceMessageId" | "eventId";
};
target: {
type: "lambda";
partialBatchFailure: true; // required for SQS/Kinesis batches
maxConcurrency?: number;
};
dlq: { arn: string; maxReceiveCount: number };
};
// ❌ “We’ll add DLQ later” is how silent loss ships
export const ORDER_PIPE: PipeContract = {
source: "sqs",
filter: {
matchEventTypes: ["order.created", "order.updated"],
onUnknownField: "drop",
},
enrichment: {
type: "lambda",
timeoutMs: 3000,
onError: "retry_then_dlq",
idempotencyKey: "sourceMessageId",
},
target: { type: "lambda", partialBatchFailure: true, maxConcurrency: 50 },
dlq: { arn: "arn:aws:sqs:REGION:ACCOUNT:orders-pipe-dlq", maxReceiveCount: 3 },
};
order.cancelled, the filter must be updated deliberately — see additive consumer patterns in EventBridge Schema Evolution.
Filters: fail closed on shape, not on vibes
EventBridge filter patterns are powerful and easy to overfit. Prefer matching on stable envelope fields (detail-type, source, typed ids). Matching deep optional JSON paths is how a producer field rename silently zeros throughput.
typescript
Operational rule: every filter change ships with at least one should-match and one should-drop fixture in CI. Without that, you are editing production drop logic by eye.
// infra/order-pipe-filter.ts — CDK-ish filter pattern (illustrative)
export const orderCreatedFilter = {
// ✅ Envelope + required business keys
"detail-type": ["order.created", "order.updated"],
source: ["com.cheatcoders.orders"],
detail: {
orderId: [{ "exists": true }],
status: ["PENDING", "CONFIRMED"],
},
};
// ❌ Fragile: nested optional promo that producers sometimes omit
export const fragilePromoFilter = {
detail: {
promo: {
code: [{ "prefix": "SUMMER" }],
},
},
};
// Unit-test patterns against fixtures — Pipe filter changes are deploys
export function assertFilterFixtures(
pattern: Record<string, unknown>,
fixtures: { name: string; event: unknown; expectMatch: boolean }[],
) {
for (const f of fixtures) {
const matched = eventBridgePatternMatches(pattern, f.event); // your tester
if (matched !== f.expectMatch) {
throw new Error(`filter_fixture_failed:${f.name}`);
}
}
}
Enrichment: IAM, timeouts, and never rewrite identity
Enrichment Lambda/API calls run in the hot path before the target. Give enrichment a dedicated role that can onlyGetItem / GetSecret for what it needs — not the target’s write permissions. Cap timeout well below the source visibility timeout so retries remain possible.
typescript
Reuse the same DynamoDB idempotency discipline from Lambda Powertools Idempotency if enrichment ever writes. Prefer read-only enrichment so retries are free.
// enrichment/enrich-order.ts
import { DynamoDBClient, GetItemCommand } from "@aws-sdk/client-dynamodb";
import { createHash } from "node:crypto";
const ddb = new DynamoDBClient({});
const TABLE = process.env.CUSTOMER_TABLE!;
export type PipeRecord = {
orderId: string;
customerId: string;
status: string;
};
export type Enriched = PipeRecord & {
tier: "free" | "pro" | "enterprise";
enrichmentVersion: 1;
};
export async function handler(event: { orderId: string; customerId: string; status: string }): Promise<Enriched> {
// ✅ Preserve source identity keys; only ADD fields
// ❌ Never: regenerate orderId, coerce types, or drop status
const out = await ddb.send(
new GetItemCommand({
TableName: TABLE,
Key: { pk: { S: `CUST#${event.customerId}` } },
ProjectionExpression: "tier",
}),
);
if (!out.Item?.tier?.S) {
// Fail closed: throw so Pipe retry/DLQ path engages
// ❌ return { ...event, tier: "free" } on miss — invents business state
throw new Error(`customer_tier_missing:${event.customerId}`);
}
return {
...event,
tier: out.Item.tier.S as Enriched["tier"],
enrichmentVersion: 1,
};
}
// Idempotency for enrichment side effects (if any writes):
export function enrichKey(msgId: string) {
return createHash("sha256").update(`pipe-enrich:v1:${msgId}`).digest("hex");
}
Target Lambda: partial batch failure or you reprocess ghosts
SQS and Kinesis Pipes can batch. If your target returns success for the whole batch after one record throws, you either lose the bad record or reprocess the good ones forever. Report failures explicitly.typescript
In the Pipe/target configuration, enable the report-batch-item-failures feature and set reserved concurrency so a thundering enrichment cache miss does not melt DynamoDB. For cold-path latency budgets on the target, the measurement playbook in Lambda Cold Starts on Node 20 still applies.
// target/process-orders.ts
import type { SQSBatchResponse, SQSEvent } from "aws-lambda";
export async function handler(event: SQSEvent): Promise<SQSBatchResponse> {
const batchItemFailures: { itemIdentifier: string }[] = [];
for (const rec of event.Records) {
try {
const body = JSON.parse(rec.body) as { orderId: string; tier: string };
await applyOrder(body);
} catch (err) {
console.error("order_apply_failed", {
messageId: rec.messageId,
err: err instanceof Error ? err.message : String(err),
});
// ✅ Only failed ids — good records commit
batchItemFailures.push({ itemIdentifier: rec.messageId });
}
}
return { batchItemFailures };
}
async function applyOrder(_o: { orderId: string; tier: string }) {
// business write — must be idempotent on orderId
}
Observability: prove drops are intentional
Pipes emit metrics; most teams never chart them next to source depth. Minimum dashboard:- Source:
ApproximateNumberOfMessagesVisible(SQS) or iterator age (Kinesis) - Pipe: invocations, enrichment errors/timeouts, target failures
- Ratio: matched vs source receive rate (sudden drop = filter regression)
- DLQ depth + age alarm
typescript
Before cutover, dual-run: keep the old router Lambda writing to a shadow log, compare event ids processed by Pipe vs router for 24–72h. Only then delete the glue.
// alerts/pipe-slo.ts — illustrative thresholds
export const PIPE_ALERTS = [
{ metric: "EnrichmentErrors", window: "5m", threshold: 5, action: "page" },
{ metric: "TargetFailures", window: "5m", threshold: 10, action: "page" },
{ metric: "DlqVisible", window: "1m", threshold: 1, action: "ticket" },
// ✅ MatchedRate drop >50% vs 24h baseline after deploy → auto-rollback candidate
{ metric: "MatchedRateDropPct", window: "15m", threshold: 50, action: "page" },
];
Checklist before you delete the router Lambda
- [ ] Pipe contract documents filter drop vs DLQ vs fail for enrichment errors
- [ ] Filter fixtures in CI (match + drop) for every pattern change
- [ ] Enrichment IAM is read-scoped; timeout < source visibility / shard retry budget
- [ ] Enrichment never mutates identity keys; versioned enrichment payload
- [ ] Target reports
batchItemFailures; handler idempotent on business key - [ ] DLQ + alarm on depth/age; enrichment/target error alarms
- [ ] Matched-rate dashboard vs baseline; dual-run comparison before cutover
- [ ] Schema/filter changes follow additive rules (schema evolution guide)
Related reading
- EventBridge Schema Evolution: Additive Changes Without Breaking Consumers
- Lambda Powertools Idempotency: DynamoDB Keys That Survive Retries
- Lambda Cold Starts on Node 20: Measure, Cut, and Keep Cutting
- Human-in-the-Loop Gates: Dual Control for Prod-Touching Agent Tools
Related reading
Watch: 100 Days of AI on YouTube
Short videos from CheatCoders, one AI engineering topic per day.
Most viewed
- Python Decorators Explained: From Simple Wrappers to Production Patterns
- Claude Code vs Cursor vs GitHub Copilot: Choose by Workflow, and Set Up One Repo That Works With All Three
- AI Agent Frameworks in 2025: LangGraph vs CrewAI vs AutoGen vs Raw API
- Python String Methods: Every str Method With Real Production Examples
- Css Animation Performance: Performance-Optimized Motion for Modern UIs
Newly added
- AWS Systems Manager Parameter Store: Hierarchical Runtime Config for Coding Agents
- Amazon Cognito Identity Pools: Federate Coding-Agent Tool Callers Without Long-Lived Keys
- Amazon Athena for Coding Agents: Query Tool Audit Logs and Eval Traces on S3 Without a Warehouse
- AWS Batch for Coding Agents: Parallel Overnight PR Reviews and Long-Running Jobs Without Lambda Timeouts
- Amazon Bedrock Prompt Management: Version and A/B Test Coding-Agent System Prompts
Deep-dive PDF
Get the expanded guide for this post — extra diagrams-style checklists, failure modes, and a production walkthrough. Free when you subscribe to CheatCoders.
Already subscribed? or open the subscribe page.
Discover more from CheatCoders
Subscribe to get the latest posts sent to your email.