1---
2title: Event Sources
3description: An event source is a binding that runs your Function when something happens on a resource — one call wires the event-source mapping, the permissions, and a typed handler.
4---
5
6An **Event Source** is a [Binding](/infrastructure-as-effects/binding)
7that runs your Function when something happens on a resource. A message
8lands on a queue, an object lands in a bucket, a row changes in a table.
9You name the resource and a handler, and the messages arrive as an
10Effect `Stream`:
11
12```typescript
13import * as AWS from "alchemy/AWS";
14import * as Effect from "effect/Effect";
15import * as Stream from "effect/Stream";
16
17export const Orders = AWS.SQS.Queue("Orders");
18
19export default AWS.Lambda.Function(
20 "OrderProcessor",
21 { main: import.meta.url },
22 Effect.gen(function* () {
23 yield* AWS.SQS.consumeQueueMessages(Orders, (records) =>
24 records.pipe(
25 Stream.map((record) => record.body),
26 Stream.runForEach((body) => Effect.log(`order: ${body}`)),
27 ),
28 );
29 }).pipe(Effect.provide(AWS.Lambda.QueueEventSource)),
30);
31```
32
33A pure consumer returns nothing from its constructor. It only declares
34what it listens to.
35
36## What one call wires
37
38In traditional IaC, running code when a message lands takes three
39pieces: permission to receive and delete messages, a mapping that tells
40the queue to invoke the Function, and a handler that unpacks the raw
41event:
42
43```typescript
44// infrastructure: grant access and wire the trigger
45policy: { Action: ["sqs:ReceiveMessage", "sqs:DeleteMessage", "sqs:GetQueueAttributes"], Resource: [queue.arn] },
46eventSourceMapping: { eventSourceArn: queue.arn, functionName: fn.name },
47```
48
49```typescript
50// handler: unpack the batch by hand
51export const handler = async (event) => {
52 for (const record of event.Records) {
53 console.log(record.body);
54 }
55};
56```
57
58`consumeQueueMessages` does all three. At deploy time it attaches that
59statement to the Function's role, scoped to the queue's ARN, and
60creates the event source mapping that points the queue at this
61Function. At runtime it registers the listener that turns each
62invocation's batch into `records`.
63
64`consumeQueueMessages` is the contract and `AWS.Lambda.QueueEventSource`
65is the Layer that satisfies it, the same split every binding has
66([a contract and a Layer](/infrastructure-as-effects/binding#a-contract-and-a-layer)).
67
68## It's a real Effect Stream
69
70`records` is the same `Stream` from any Effect program, so every
71combinator composes. Map table changes to JSON and run them straight
72into a queue:
73
74```typescript
75const sink = yield* AWS.SQS.QueueSink(Outbound);
76
77yield* AWS.DynamoDB.consumeTableChanges(
78 Jobs,
79 { streamViewType: "NEW_AND_OLD_IMAGES", startingPosition: "TRIM_HORIZON" },
80 (stream) =>
81 stream.pipe(
82 Stream.map((record) =>
83 JSON.stringify({
84 eventName: record.eventName,
85 keys: record.dynamodb.Keys,
86 }),
87 ),
88 Stream.run(sink),
89 ),
90);
91```
92
93`Stream.throttle` or `Stream.groupedWithin` drop in anywhere along the
94chain. That `sink` is a [Sink](/infrastructure-as-effects/sinks), the
95write-side dual of an event source. Its permissions are generated the
96same way.
97
98## The same shape on Cloudflare
99
100Consume a Cloudflare queue the same way. The consumer settings go on
101the call, and the handler receives a `Stream` of typed messages:
102
103```typescript
104import * as Cloudflare from "alchemy/Cloudflare";
105import * as Effect from "effect/Effect";
106import * as Stream from "effect/Stream";
107import * as HttpServerResponse from "effect/http/HttpServerResponse";
108
109export const Orders = Cloudflare.Queues.Queue("Orders");
110
111export default Cloudflare.Worker(
112 "OrderProcessor",
113 { main: import.meta.url },
114 Effect.gen(function* () {
115 yield* Cloudflare.Queues.consumeQueueMessages<{ text: string }>(
116 Orders,
117 { batchSize: 10, maxRetries: 3, retryDelay: "1 second" },
118 (stream) =>
119 Stream.runForEach(stream, (msg) => Effect.log(msg.body.text)),
120 );
121
122 return {
123 fetch: Effect.gen(function* () {
124 return HttpServerResponse.text("ok");
125 }),
126 };
127 }).pipe(Effect.provide(Cloudflare.Queues.EventSourceLive)),
128);
129```
130
131There is no IAM here. At deploy time the call creates the queue
132consumer that dispatches batches to this Worker. At runtime it
133registers the `queue` listener.
134
135## Batching and retries
136
137Set `batchSize` and `maximumBatchingWindow` on the call to shape each
138invocation's batch:
139
140```typescript
141yield* AWS.SQS.consumeQueueMessages(
142 Orders,
143 { batchSize: 10, maximumBatchingWindow: "5 seconds" },
144 (records) => Stream.runForEach(records, (record) => Effect.log(record.body)),
145);
146```
147
148If your handler fails, the queue redelivers the batch. Stream sources
149like Kinesis and DynamoDB add settings that bound a poison record's
150blast radius:
151
152```typescript
153yield* AWS.DynamoDB.consumeTableChanges(
154 Jobs,
155 {
156 streamViewType: "NEW_AND_OLD_IMAGES",
157 startingPosition: "TRIM_HORIZON",
158 bisectBatchOnFunctionError: true,
159 maximumRetryAttempts: 3,
160 },
161 (stream) => Stream.runForEach(stream, (record) => Effect.log(record.eventName)),
162);
163```
164
165On Cloudflare, set the retry policy on the call. A batch is acked when
166your handler succeeds, retried `maxRetries` times when it fails, then
167dead-lettered:
168
169```typescript
170yield* Cloudflare.Queues.consumeQueueMessages<{ text: string }>(
171 Orders,
172 { maxRetries: 3, retryDelay: "1 second", deadLetterQueue: "orders-dlq" },
173 (stream) => Stream.runForEach(stream, (msg) => Effect.log(msg.body.text)),
174);
175```
176
177Call `msg.ack()` or `msg.retry()` inside the handler to decide per
178message.
179
180## Not every source is a Stream
181
182A source that delivers one event at a time hands your handler the event
183directly. A cron trigger passes one `controller` per fire:
184
185```typescript
186export default Cloudflare.Worker(
187 "Nightly",
188 { main: import.meta.url },
189 Effect.gen(function* () {
190 yield* Cloudflare.Workers.cron("0 12 * * *", (controller) =>
191 Effect.log(`scheduled at ${controller.scheduledTime}`),
192 );
193
194 return {
195 fetch: Effect.gen(function* () {
196 return HttpServerResponse.text("ok");
197 }),
198 };
199 }).pipe(Effect.provide(Cloudflare.Workers.CronEventSourceLive)),
200);
201```
202
203At deploy time the cron expression is attached to the Worker. At runtime
204the `scheduled` listener is registered. GitHub works the same way:
205`consumeRepositoryEvents` creates a repository webhook pointing at the
206Worker's URL, and the runtime listener verifies each delivery's HMAC
207signature before calling your handler.
208
209## Every event source
210
211The shape never changes. Only the `consume*` callable, the record type,
212and the Layer you provide differ:
213
214```typescript
215// AWS: handlers receive a Stream per batch
216yield* AWS.SQS.consumeQueueMessages(queue, fn);
217yield* AWS.Kinesis.consumeStreamRecords(stream, props, fn);
218yield* AWS.DynamoDB.consumeTableChanges(table, props, fn);
219yield* AWS.S3.consumeBucketEvents(bucket, fn);
220yield* AWS.SNS.consumeTopicNotifications(topic, fn);
221yield* AWS.EventBridge.consumeBusEvents(bus, pattern, fn);
222
223// Cloudflare
224yield* Cloudflare.Queues.consumeQueueMessages(queue, fn);
225yield* Cloudflare.Workers.cron(expression, fn);
226yield* GitHub.consumeRepositoryEvents(props, fn);
227```
228
229EventBridge can also route matching events to another resource instead
230of consuming them locally:
231
232```typescript
233yield* AWS.EventBridge.events(bus, { source: ["my.app"] }).toQueue(queue);
234```
235
236Each source has its own guide. On AWS: [SQS](/aws/messaging/sqs),
237[Kinesis](/aws/messaging/kinesis),
238[DynamoDB Streams](/aws/messaging/dynamodb-streams),
239[S3 Events](/aws/messaging/s3-events), [SNS](/aws/messaging/sns), and
240[EventBridge](/aws/messaging/eventbridge). On Cloudflare:
241[Queues](/cloudflare/messaging/queues), [Cron](/cloudflare/messaging/cron),
242and [GitHub Events](/cloudflare/messaging/github-events).
243
244## Where next
245
246- [Sinks](/infrastructure-as-effects/sinks) — the write-side dual:
247 resources as Effect `Sink`s, with batching and IAM generated the
248 same way.
249- [Bindings](/infrastructure-as-effects/binding) — the deploy-time
250 mechanics every event source is built on.
251- [Runtime](/infrastructure-as-effects/runtime) —
252 the Effectful Constructor these examples live inside.