Skip to content

Flow Control ​

Taskora provides three flow control strategies — debounce, throttle, and deduplicate — all implemented as atomic Lua scripts.

Debounce0 enqueued
Replaces previous — only last fires
Throttle0 enqueued
0/3 in window
Deduplicate0 enqueued
Job idle

Debounce ​

Replace the previous pending job for the same key. Only the last dispatch within the delay window is processed.

ts
searchIndexTask.dispatch(data, {
  debounce: {
    key: `reindex:${documentId}`,
    delay: "2s",
  },
})

Use case: Avoid redundant reindexing when a document is edited multiple times in quick succession. Only the final version is indexed.

How It Works ​

  1. First dispatch creates a delayed job
  2. Subsequent dispatches with the same key replace the previous job (reset the delay timer)
  3. When the delay elapses without a new dispatch, the job moves to the waiting queue

Throttle ​

Rate-limit dispatches per key. Excess dispatches are rejected.

ts
const handle = callExternalApiTask.dispatch(data, {
  throttle: {
    key: "stripe-api",
    max: 100,        // max 100 dispatches
    window: "1m",    // per 1 minute window
  },
})

if (!handle.enqueued) {
  console.log("Rate limited — try again later")
}

Use case: Respect external API rate limits by capping how many jobs can be enqueued in a time window.

How It Works ​

  1. Each dispatch increments a counter for the key
  2. Counter resets when the window expires
  3. If counter exceeds max, the dispatch is rejected (handle.enqueued = false)
  4. With throwOnReject: true, throws ThrottledError instead

Deduplicate ​

Skip dispatch if a job with the same key already exists in a matching state.

ts
const handle = generateReportTask.dispatch(data, {
  deduplicate: {
    key: `report:${userId}`,
    while: ["waiting", "active"], // default: ["waiting", "delayed", "active"]
  },
})

if (!handle.enqueued) {
  console.log("Report already in progress:", handle.existingId)
}

Use case: Prevent duplicate report generation when a user clicks "Generate" multiple times.

How It Works ​

  1. Checks if a job with the same dedup key exists in any of the while states
  2. If found, returns the existing job's ID and sets handle.enqueued = false
  3. If not found, creates a new job and stores the dedup key
  4. Dedup keys are automatically cleaned up when jobs complete or fail

Throwing on Rejection ​

By default, throttle and dedup silently reject. Use throwOnReject for explicit error handling:

ts
try {
  sendEmailTask.dispatch(data, {
    throttle: { key: "emails", max: 100, window: "1h" },
    throwOnReject: true,
  })
} catch (err) {
  if (err instanceof ThrottledError) {
    // err.key = "emails"
  }
  if (err instanceof DuplicateJobError) {
    // err.key, err.existingId
  }
}

Combining Flow Control ​

Flow control options are mutually exclusive — use only one per dispatch call. Collect tasks are also mutually exclusive with all flow control options.

Released under the MIT License.