Debounce
Debounce Usage Guide
Section titled “Debounce Usage Guide”Debounce collapses rapid-fire events into a single function invocation. The handler fires once with the most recent event payload, after the quiet period elapses with no new events.
Use it when:
- A webhook source bursts (GitHub push storms, Stripe retries, IoT sensor streams).
- A user action repeats faster than you want to react (search-as-you-type, “save while typing” autosave).
- An upstream system emits one logical change as N physical events.
How It Works
Section titled “How It Works”events: A B C D E │ │ │ │ │ ▼ ▼ ▼ ▼ ▼time: ───┼───┼───┼────────┼─────────fires──┼─────────fires─► ◀── 5s reset window ─▶◀── 5s window ──▶ (collapsed C) (collapsed E)The first event in a window arms a timer. Each subsequent event for the same key resets the timer and replaces the queued payload. When the quiet period elapses with no new events, the function fires once.
Per-key isolation. Configure a key (JSON path into the event payload) to debounce independently per group. Events for userId=u1 and userId=u2 get independent timers.
No key = global lane. Without a key, all events for the function collapse into a single debounce lane.
Define a Debounced Function
Section titled “Define a Debounced Function”TypeScript (Node SDK)
Section titled “TypeScript (Node SDK)”import { ironflow } from "@ironflow/node";
const processSearch = ironflow.createFunction( { id: "process-search", triggers: [{ event: "search.requested" }], debounce: { periodMs: 5000, // 5 seconds of quiet before firing key: "userId", // independent per user }, }, async ({ event, step }) => { const results = await step.run("search", async () => { return performSearch(event.data.query); }); return { results }; });Go SDK
Section titled “Go SDK”import ( "time"
"github.com/sahina/ironflow/sdk/go/ironflow")
var ProcessSearch = ironflow.CreateFunction(ironflow.FunctionConfig{ ID: "process-search", Triggers: []ironflow.Trigger{{Event: "search.requested"}}, Debounce: &ironflow.DebounceConfig{ Period: 5 * time.Second, Key: "userId", },}, func(ctx ironflow.Context) (any, error) { // ... return nil, nil})Configuration
Section titled “Configuration”| Field | Type | Required | Description |
|---|---|---|---|
periodMs (TS) / Period (Go) | int / time.Duration | yes | Quiet period before firing. Floor: 1000 ms (1 second). Sub-second values are rejected at registration time because the scheduler tick floor is 1s. |
key (TS) / Key (Go) | string | no | JSON path for per-key debouncing (e.g., "userId", "data.customerId"). Same extraction rules as concurrency.key. Empty key = global lane. |
maxWaitMs (TS) / MaxWait (Go) | int / time.Duration | no | Starvation cap. Handler fires at least once every maxWait even if resets never stop arriving. Must be >= period when set. Omit (or 0) for no cap. See Starvation Cap. |
Starvation Cap (maxWait)
Section titled “Starvation Cap (maxWait)”Without maxWait, a continuous storm of events at intervals shorter than period resets the timer forever — the handler never fires. Set maxWait to guarantee a maximum delay between fires:
debounce: { periodMs: 5000, // normal: fire 5s after last event maxWaitMs: 60000, // never wait more than 60s, no matter what key: "userId",}Semantics:
- The first event in a window arms two deadlines:
firesAt = now + periodandmaxFireAt = now + maxWait. - Each reset advances
firesAtbut does NOT advancemaxFireAt— the cap is anchored to the first event. - The sweep fires when
now >= firesAtORnow >= maxFireAt, whichever comes first. - After fire, both deadlines clear. The next event starts a fresh window.
Use it for search-as-you-type, IoT streams, or any source that may legitimately never go quiet but where the user still expects periodic feedback.
Synchronous Triggers Are Rejected
Section titled “Synchronous Triggers Are Rejected”Debounce is asynchronous by design — the function fires after a delay, not during the original call. Calling TriggerSync on a debounced function returns FailedPrecondition:
function "process-search" has debounce config; TriggerSync is incompatiblewith debounce — use Trigger (async)Use the async Trigger / emit API for debounced functions.
Cluster Behavior
Section titled “Cluster Behavior”Debounce state lives in a NATS KV bucket (SYS_debounce_state) replicated across cluster nodes. The first event in a window claims the entry via a kv.Create first-writer-wins; subsequent events CAS-update the same entry, so two nodes racing to arm the same key produce exactly one debounce timer.
When the timer expires, the sweep loop on any node claims the entry via a CAS DeleteRev. Exactly one node fires the run cluster-wide.
A crash between claim and run creation is recovered via the pending_debounce_fires outbox table. See the debounce explanation for the full reliability model.
Function Version Invalidation
Section titled “Function Version Invalidation”If you update a function’s config mid-window (UpdateFunction or re-registering with RegisterFunction), in-flight debounce entries armed against the old version are dropped without firing. The next event arms a fresh entry against the new version. This prevents firing a stale handler shape against a payload that was queued for a different config.
Inspecting and Cancelling Entries
Section titled “Inspecting and Cancelling Entries”List pending entries (env-scoped):
ironflow debounce listironflow debounce list --jsonCancel a pending entry without firing it:
ironflow debounce cancel my-fn-id user-42ironflow debounce cancel my-fn-id "build:repo-7" --env env_default--env is optional. When omitted, the cancel command list-then-matches the entry uniquely on (function_id, debounce_key). Multiple matches across envs error out with a “pass —env to disambiguate” hint — refuses to guess across env boundaries.
See the CLI reference for full flag documentation.
HTTP API
Section titled “HTTP API”GET /api/v1/debounce/entriesDELETE /api/v1/debounce/entries/{envID}/{fnID}/{key}The key path segment must be base64url-encoded so debounce keys containing / or . round-trip correctly. The CLI encodes for you. See the REST API reference for response shapes.
Trade-offs
Section titled “Trade-offs”What you get: events stop being noise, handlers run once per logical change, no rate-limit dance against downstream APIs.
What you give up:
- Latency. The handler fires
periodafter the last event, not the first. Pick a period that matches user expectations (5s for autosave, 30s for batched email digests, several minutes for daily-rollup batches). - Loss of intermediate payloads. Only the most recent event’s payload reaches the handler. If you need every event, do not debounce.
- Indefinite postponement under continuous activity unless you set
maxWait(see above).
See Also
Section titled “See Also”- Concurrency — limit parallel executions; complementary to debounce
- Circuit Breakers — protect downstream endpoints from cascading failures
- Execution Modes — push vs pull and what debounce means for each