# Keyed debounce

> Replace pending work under a stable key while updates continue arriving.

Debounce keeps one pending job with the latest accepted payload. PostgreSQL serializes concurrent
replacements, so every caller observes one stable job identity.

<Tabs items={["TypeScript", "Python", "Go"]}>
  <Tab value="TypeScript">
    ```ts
    const result = await queue.enqueueWithResult("search.reindex", { documentId, revision }, {
      debounce: { key: documentId, scope: "search-index", windowMs: quietPeriodMs, schedule: "reset" },
    });
    ```
  </Tab>
  <Tab value="Python">
    ```python
    result = queue.enqueue_with_result(
        "search.reindex",
        {"documentId": document_id, "revision": revision},
        EnqueueOptions(debounce=Debounce(
            key=document_id, scope="search-index", window_ms=quiet_period_ms, schedule="reset"
        )),
    )
    ```
  </Tab>
  <Tab value="Go">
    ```go
    result, err := queue.EnqueueWithResult(ctx, "search.reindex", map[string]any{
        "documentId": documentID, "revision": revision,
    }, workhorse.EnqueueOptions{Debounce: &workhorse.Debounce{
        Key: documentID, Scope: "search-index", WindowMS: quietPeriodMS, Schedule: workhorse.DebounceReset,
    }})
    ```
  </Tab>
</Tabs>

Choose `reset` to start a fresh quiet period after every replacement. Choose `preserve` when the
first request fixes the run time and later requests should update only the payload.

`result.outcome` is `accepted` for a new job and `replaced` for a pending update. Once a worker owns
the job, it becomes terminal, or its window elapses, PostgreSQL returns `non_replaceable` and keeps
the accepted payload unchanged.

Only `scheduled` or `ready` jobs can be replaced. A refused replacement includes a stable `reason`
so callers can distinguish lifecycle movement from an incompatible request.

Debounce cannot share a request with idempotency, throttle, or dependencies because each mechanism
assigns a different meaning to a repeated key or immutable edge.

The result's `jobId` identifies the retained pending job. When replacement depends on earlier work,
`prerequisiteJobId` identifies that immutable dependency instead of the debounce candidate.

## Next

- [Throttle](/docs/throttle) — reuse equivalent work without replacing it
- [Idempotent enqueue](/docs/idempotency) — replay an identical request
- [Job dependencies](/docs/job-dependencies) — keep accepted work blocked

---

Exact debounce outcomes, limits, and lifecycle events:
[architecture reference](https://github.com/stablemates/workhorse/blob/main/docs/architecture.md#keyed-debounce).
