Skip to content

triggers: add a debounce / minimum fire interval #3060

Description

@williamhbaker

Summary

Runtime webhook triggers fire once per committed materialization transaction that
materializes data
, with no rate-limiting. When several transactions commit in close
succession, this produces a burst of webhook deliveries. The older connector-side
dbt-job trigger avoided this with a debounce that collapses repeated triggers into
at most one per configured interval. That debounce was never ported to the more general
runtime triggers. We should add an equivalent.

Background

Runtime triggers are the general replacement for the connector-specific dbt-job trigger:
any materialization can POST/PUT/PATCH a templated payload to a webhook when it
materializes new data (e.g. to kick off a downstream dbt Cloud run). Config is
models::TriggerConfigurl, method, headers, payload_template, timeout,
max_attempts. There is no interval / debounce field today
(crates/models/src/triggers.rs:24).

Current firing behavior

A trigger fires for every committed transaction that materialized at least one document:

  • V1 (current production): trigger variables are computed only when the transaction
    had data — compiled_triggers.is_some() && !txn.stats.is_empty()
    (crates/runtime/src/materialize/serve.rs:253), persisted to RocksDB at StartCommit
    for at-least-once delivery, then delivered when the connector sends Acknowledged
    (crates/runtime/src/materialize/protocol.rs:385).
  • V2 (runtime-next): same gate —
    task.triggers.is_some() && !extents.bindings.is_empty()
    (crates/runtime-next/src/leader/materialize/fsm.rs:642), delivered in
    crates/runtime-next/src/leader/materialize/actor.rs:451.

There is no debounce, batching, or minimum interval anywhere on this path:
N qualifying transactions → N webhook deliveries.

Problem

The canonical use of a trigger is to start an expensive downstream job (a dbt run). One
delivery per transaction means that anything producing several transactions in a short
window — catch-up after a skipped/paused sync interval, a frequent sync cadence, or a
backfill — produces a burst of webhook calls and a burst of downstream job runs. That's
the user-reported symptom ("multiple triggers when a sync schedule is skipped"): the
debounce is the general fix regardless of the exact sequence that produced the burst.

Prior art: the connector-side dbt debounce

The dbt-job trigger already solved this. After each Acknowledged it signals a
background handler that enforces a minimum interval between actual job triggers
(connectors/go/materialize/transactions_stream.go:154, default interval = 30m,
"only triggered if data has been materialized"):

// dbtJobTriggerHandler: interval-based debounce
//   lastRun          time.Time   // when a job last actually fired
//   nextRunScheduled atomic.Bool // a deferred fire is already queued
//
// On each Acknowledged signal:
//   - if a run is already scheduled        -> do nothing (collapses the burst)
//   - else if now - lastRun >= interval    -> fire immediately
//   - else                                 -> schedule one fire at lastRun + interval

Multiple acknowledgements within the interval window collapse into a single eventual
trigger. This logic is connector-local and was never generalized to runtime triggers.

Proposed solution

Add a minimum-interval / debounce to runtime triggers so that, per trigger config, at
most one delivery occurs per interval, collapsing bursts of qualifying transactions —
while preserving the existing semantics (fire only when data was materialized;
at-least-once delivery).

We only need to implement for Runtime V2.

Likely the smallest viable shape:

  • Add an optional interval (minimum fire interval) field to TriggerConfig.
  • On the firing path, suppress a delivery if one fired within interval; ensure a
    pending fire still happens eventually (don't silently drop it).

References

  • Trigger config (no interval field today): crates/models/src/triggers.rs:24
  • V2 fire gate / delivery: crates/runtime-next/src/leader/materialize/fsm.rs:642,
    crates/runtime-next/src/leader/materialize/actor.rs:451
  • Connector dbt debounce (prior art): connectors/go/materialize/transactions_stream.go:154,
    connectors/go/dbt/trigger.go

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions