Skip to main content
A Debounce job collects incoming events and processes them as a batch after a delay or when a maximum event count is reached. Each instance accumulates events in its Durable Object and flushes them to your execute handler at the right time. Use Debounce jobs when many events arrive in rapid succession but you only want to process them once: webhook coalescing, search-as-you-type, and update batching.

Defining a debounce job

import { Effect, Schema } from "effect";
import { Debounce } from "@durable-effect/jobs";

const webhookBatcher = Debounce.make({
  eventSchema: Schema.Struct({
    type: Schema.String,
    contactId: Schema.String,
    data: Schema.Unknown,
  }),

  stateSchema: Schema.Struct({
    events: Schema.Array(Schema.Unknown),
    count: Schema.Number,
  }),

  flushAfter: "5 seconds",
  maxEvents: 100,

  onEvent: (ctx) =>
    Effect.succeed({
      events: [...ctx.state.events, ctx.event],
      count: ctx.state.count + 1,
    }),

  execute: (ctx) =>
    Effect.gen(function* () {
      const state = yield* ctx.state;
      const count = yield* ctx.eventCount;

      console.log(`Flushing ${count} events, reason: ${ctx.flushReason}`);
      yield* sendWebhookBatch(state.events);
    }),
});

Configuration

eventSchema
Schema.Schema<I>
required
Effect Schema for validating incoming events. Each event added via client.debounce(...).add() is validated against this schema.
execute
(ctx: DebounceExecuteContext<S>) => Effect<void, E, never>
required
Handler called when the batch flushes. Receives the accumulated state and flush metadata. Must return Effect<void, E, never>.
flushAfter
Duration.DurationInput
required
How long to wait after the first event before flushing. The timer resets if maxEvents is not reached.
stateSchema
Schema.Schema<S>
Optional schema for the persisted state type. Defaults to eventSchema, which causes the state to be overwritten with the latest event on each onEvent call.
maxEvents
number
Optional maximum number of events to accumulate before flushing immediately, even if flushAfter has not elapsed.
onEvent
(ctx: DebounceEventContext<I, S>) => Effect<S, never, never>
Optional reducer called for each incoming event. Returns the new accumulated state. Default: replace state with the latest event.
retry
JobRetryConfig
Optional retry configuration for execute failures. Events remain in the buffer during retry attempts.
logging
boolean | LogLevel
Control logging verbosity. false (default) logs only errors. true enables debug-level logging.

Flush triggers

A Debounce job flushes its accumulated events when any of the following conditions are met:
flushReasonWhen
"flushAfter"The configured delay has elapsed since the first event arrived
"maxEvents"The number of accumulated events has reached maxEvents
"manual"client.debounce(...).flush() was called directly

Execute context

PropertyTypeDescription
stateEffect<S>Accumulated state. Yield to read.
eventCountEffect<number>Total number of events received in this batch.
flushReason"maxEvents" | "flushAfter" | "manual"What triggered this flush.
debounceStartedAtEffect<number>Timestamp (ms) when the first event arrived.
attemptnumberCurrent retry attempt (1 = first try).
isRetrybooleanWhether this flush is a retry.

onEvent context

The onEvent handler receives a context with the incoming event and current accumulated state.
PropertyTypeDescription
eventIThe incoming event (already validated).
stateSCurrent accumulated state before this event.
eventCountnumberNumber of events received so far (before this one).
instanceIdstringDurable Object instance ID.
On the first event, state is initialized to the event value itself when no stateSchema is provided. If you provide a custom stateSchema, ensure your onEvent handler handles the initial state correctly.

Complete example

import { Effect, Schema } from "effect";
import { Debounce } from "@durable-effect/jobs";
import { createDurableJobs } from "@durable-effect/jobs";

const webhookBatcher = Debounce.make({
  eventSchema: Schema.Struct({
    type: Schema.String,
    contactId: Schema.String,
    data: Schema.Unknown,
  }),

  stateSchema: Schema.Struct({
    events: Schema.Array(Schema.Unknown),
    count: Schema.Number,
  }),

  flushAfter: "5 seconds",
  maxEvents: 100,

  onEvent: (ctx) =>
    Effect.succeed({
      events: [...ctx.state.events, ctx.event],
      count: ctx.state.count + 1,
    }),

  execute: (ctx) =>
    Effect.gen(function* () {
      const state = yield* ctx.state;
      const count = yield* ctx.eventCount;

      console.log(`Flushing ${count} events, reason: ${ctx.flushReason}`);
      yield* sendWebhookBatch(state.events);
    }),
});

const { Jobs, JobsClient } = createDurableJobs({ webhookBatcher });
export { Jobs };

Build docs developers (and LLMs) love