Debounce
Description
Debounces events independently within each group, using changes in the observed Value to start and reset the change cycle. Groups are defined by Key by: a change of the observed value opens a cycle, further changes reset the quiet period (flapping), and the cycle closes once the value has stayed unchanged for the Quiet period (or when Max cycle duration caps the cycle under continuous flapping). Emit event selects which event of a cycle is released: the first one (on the change), the last one (after the value settles), or both. Events that do not survive the quiet period are deliberately dropped; a value that stays settled is emitted once, and repeats of the settled value are ignored (conflation).
The component declares no output variable and passes all scenario variables through. It is a gate, not a mapping - emitted events are passed through unchanged.
Use cases
- Anti-flapping in alerting: fire only once a status has held for the quiet period.
- Suppressing status/health flicker before it reaches downstream consumers.
- Coalescing CDC updates: emit the row only once field X has settled on a new value.
- Smoothing sensor/IoT readings that oscillate around a threshold.
- Reacting to a settled price or configuration value rather than transient spikes.
Parameters and configuration
| Name | Description |
|---|---|
| Key by | Expression assigning each event to a group; events sharing a key are debounced independently. Example: #input.deviceId. Leave as '' to debounce the whole stream as one group. Events whose key is null are reported as errors and not emitted. |
| Value | The observed value. A change of this value starts a change cycle; the cycle closes once it has stayed unchanged for the Quiet period. null is a regular value: a transition from/to null is a change, and a stable null is emitted once. |
| Quiet period | How long the observed value must stay unchanged for the change cycle to close. With Emit event = Last/Both the last event is emitted at that moment. 0 emits on every value change (no debouncing). |
| Emit event (advanced) | First (on change) (leading edge) emits the event that introduced the new value immediately and then stays silent until the cycle closes. Last (after quiet period) (trailing edge, default) holds the most recent event and releases it once the value has stayed unchanged for the whole Quiet period. Both emits twice per cycle: the first event at the change and the last one after it settles. |
| Max cycle duration (advanced) | Upper bound on how long a single change cycle may stay open. When exceeded, the cycle is closed: the last event is emitted if Emit event is Last/Both, and the next change starts a new cycle. Guards against a continuously flapping value never settling. Empty = no limit. Must be >= Quiet period. |
| Time mode (advanced) | Processing time (wall clock, default) or Event time. |
Example
To alert only once a device status has been stable for 30 seconds:
- Key by:
#input.deviceId - Value:
#input.status - Quiet period:
30 seconds
Status flicker shorter than 30 seconds is suppressed; each settled status is emitted exactly once.
How it works
Marble example: one group, Quiet period = 5s, Emit event = Last, V is the observed value.
t=0 V=A
t=1 V=A
t=2 V=B
t=3 V=B
t=9 V=B
t=20 V=C
Aopens a cycle at t=0 but flaps toBat t=2, before 5 seconds of silence elapsed -Anever emits.Bsettles: 5 seconds pass without a change, so at t=7 the most recent event is emitted.- The repeat
Bat t=9 arrives after the value already settled - it is ignored (conflation). Cat t=20 is a change: a new cycle opens and emits at t=25.
With Emit event = First the emissions are instead the events that introduced A (t=0) and C (t=20),
each fired immediately at the change; with Both, both of the above.
Under continuous flapping the quiet period keeps resetting and silence never occurs; Max cycle duration caps the cycle length, forcing the trailing emission (Last/Both) or the re-arm (First).
First drops changes inside an open cycle
The leading edge fires once per cycle, and the silence that follows it covers every later event of that
cycle, changes included. In the marble example above, B at t=2 arrives while A's cycle is still open:
B becomes the current value but is never emitted, and once it settles its repeats are conflated away, so
that change is dropped outright rather than delayed. This is the standard leading-edge behaviour, but it
means First is a poor fit when every distinct value must be observed - use Last or Both for that.
Both can emit the same event twice
Both releases two events per cycle, and a cycle that holds only one event (the common case for a value
that changes once and then stays put) releases that single event twice. The two records are distinguished
by their context id: the leading one is tagged first and the trailing one last, the same way the
for-each component tags its fan-out. Downstream nodes therefore see two distinct correlation ids, not a
duplicate.
Restarts
In event time a restart replays the same timeline, so cycles resume exactly where they were.
In processing time the clocks of every open cycle are restarted: the Quiet period and Max cycle duration are re-measured from the moment the job comes back, as if the held event had just arrived. Without this, the deadlines checkpointed before the outage would all be in the past and would fire the instant the job resumes, releasing held events before the source has replayed what arrived during the outage - re-emitting exactly the transients the component exists to drop.
The cost is that an event pending at the moment of the restart is held for the duration of the outage plus up to one further Quiet period, because the deadline is re-anchored to the wall clock at which the job comes back rather than to when the event arrived. With a 30 second Quiet period, a 30 minute outage delays that event by roughly 30 minutes 30 seconds, not by 30 seconds. Size alerting SLAs against the expected outage window, not against the Quiet period alone.
When to use Debounce instead of a similar component
- Throttle paces events over time and never drops (rate shaping). Debounce collapses a burst of changes into a single settled emission and deliberately drops the transient ones.
- Deduplication decides on arrival: a filter condition compares the incoming event with the last accepted one for the key, and the event either passes through at once or is dropped. Debounce decides on time instead - it holds the event and lets the Quiet period pass judgement, so a value that flaps and reverts never reaches downstream at all. Use Deduplication when the rule is a comparison you can write, Debounce when the rule is "the value has to hold still".
- A session window groups by event-time gaps regardless of payload. Debounce triggers on a value change, which gives anti-flapping and conflation that a session window does not express directly.
Additional considerations
- Per-group state grows with key cardinality. The component retains the last observed value for every key for the lifetime of the job, including keys that are no longer active. This is required to ensure that a settled value is emitted only once. State for idle keys is not evicted and there is no TTL. State size therefore grows with the number of distinct keys ever seen, not with traffic: a key space of stable cardinality (device ids, tenant ids, account numbers) is fine, while an unbounded one (request ids, session ids, timestamps mixed into the key) will grow the checkpoint without limit. Key by a bounded attribute, and size the state backend for the full key space.
- The observed value must have a meaningful
equals. Change detection compares the stored value with the new one viaObjects.equals. Arrays (byte[],Object[]) use reference equality, so every event counts as a change and nothing is ever suppressed;BigDecimalis scale-sensitive, so1.0and1.00count as a change. Map such values to something comparable (aString, a list, a normalised number) in the Value expression. Note that the comparison is type-sensitive: a value that arrives once as an integer and once as a long counts as a change even when the two are numerically equal, which can happen when the expression is typed as unknown. - Changing the type of the Value is not savepoint-compatible. The serializer for the retained
value is derived from the expression's return type when the job graph is built. Editing Value
so that its type changes (for example
#input.statusto#input.statusCode) makes the checkpointed state unreadable, and a redeploy from state fails on restore. Such a change requires a redeploy without state. - Event time assumes per-key ordering. Events are processed in arrival order; strong out-of-order arrival within a key can produce spurious emissions. Kafka partitioned by the debounce key maintains the needed ordering; processing time avoids the issue entirely.
- Timer count grows with the rate of change, not just with key cardinality. Every change of the observed
value registers a timer, and superseded timers are deliberately never deleted - they fire and ignore
themselves. A flapping key therefore holds roughly
changes per second × Quiet periodlive timers in the checkpoint. At 200 changes/s with a 30 second Quiet period that is about 6000 timers for that one key, so size the state backend with the change rate in mind, not only the number of keys.
Metrics
debounce.openCycles- number of currently open change cycles (per node).debounce.delay- histogram of the hold time of trailing emissions (from the emitted event's arrival to its release), in milliseconds. Leading emissions are immediate and not recorded. A restart in processing time re-anchors the hold, so an outage is not reported as one enormous sample.
Both are reported through the standard Nussknacker metrics pipeline and tagged with nodeId and nodeName,
so they can be narrowed down to a single Debounce node. They land in the same metrics backend as the
built-in scenario metrics and can be plotted in Grafana - the shipped dashboard has no Debounce panel, so
add one. See Nussknacker metrics for the
metrics architecture and the fields each metric type reports.
Watch debounce.openCycles to tell a component that is holding events from one that has gone idle, and
debounce.delay to check how long emissions are actually held against the configured Quiet period.
debounce.openCycles is only meaningful summed across subtasks. The counter is kept per subtask and
checkpointed as evenly-redistributed operator state, while the cycles it counts live in keyed state, which is
redistributed by key group. After a change of parallelism the two no longer line up: an individual subtask can
report a value that never returns to zero, or a negative one, while the sum over all subtasks stays correct.
Plot the sum, not a single subtask.
Configuration
The component is auto-loaded and needs no configuration. To change the defaults the Designer offers for Emit event and Time mode, add a section for it in the model configuration:
components.debounce {
timeMode = "ProcessingTime" # or "EventTime"
emitEvent = "Last" # or "First" / "Both"
}
Both values are matched exactly, so they have to be spelled as above.
Either key can be set on its own; the other keeps its default. These are defaults for newly added nodes only - they do not change scenarios that are already deployed.