Skip to content

Debounce

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.
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.

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 };
}
);
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
})
FieldTypeRequiredDescription
periodMs (TS) / Period (Go)int / time.DurationyesQuiet 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)stringnoJSON 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.DurationnoStarvation 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.

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 + period and maxFireAt = now + maxWait.
  • Each reset advances firesAt but does NOT advance maxFireAt — the cap is anchored to the first event.
  • The sweep fires when now >= firesAt OR now >= 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.

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 incompatible
with debounce — use Trigger (async)

Use the async Trigger / emit API for debounced functions.

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.

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.

List pending entries (env-scoped):

Terminal window
ironflow debounce list
ironflow debounce list --json

Cancel a pending entry without firing it:

Terminal window
ironflow debounce cancel my-fn-id user-42
ironflow 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.

GET /api/v1/debounce/entries
DELETE /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.

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 period after 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).
  • 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