Runlane
Build with RunlaneControl work

Prevent duplicate runs

Use idempotency for repeated requests and singletons for overlapping work.

Runlane has two run-creation keys. They solve different problems.

KeyQuestion it answersResult on conflict
idempotencyKey“Have I already accepted this logical request?”Return the active or retained owner
singletonKey“Is work for this resource already active?”Reject a different overlapping run

Define keys from stable business identity

src/tasks/refresh-customer.ts
import { task } from '@runlane/core'
import * as z from 'zod'

const env = z.object({ CUSTOMER_API_URL: z.url() }).parse(process.env)

export const refreshCustomer = task({
  id: 'customers.refresh',
  schema: z.object({ customerId: z.string() }),
  idempotencyKey: ({ customerId }) => `refresh.${customerId}`,
  idempotencyKeyTTL: '24h',
  singletonKey: ({ customerId }) => `customer.${customerId}`,
  async run({ customerId }, context) {
    const response = await fetch(
      new URL(`/customers/${encodeURIComponent(customerId)}/refresh`, env.CUSTOMER_API_URL),
      {
        method: 'POST',
        signal: context.signal,
      },
    )
    if (!response.ok) throw new Error(`Customer API returned ${response.status}`)
  },
})

In this example:

  • repeated refresh requests for one customer return the same idempotency owner for 24 hours after it finishes.
  • different requests cannot overlap while the same customer singleton is active.

Keys are scoped by the Runlane environment and task. Use a stable domain id. Do not use the current time or a random value when you want deduplication.

Understand idempotency retention

trigger() returns an outcome:

  • created means it stored a new run.
  • returned_existing means the idempotency key already had an active or retained owner.

idempotencyKeyTTL controls terminal retention. It requires an idempotency key. If omitted, Runlane uses contractDefaults.idempotency.defaultTTL; see defaults for its current value. Set IdempotencyKeyTTLMode.Active from @runlane/core to keep ownership only while the run is active. An operator can reset a terminal owner sooner, and pruning removes its ownership.

The TTL does not expire an active owner. It controls when a terminal key may be reused.

Understand singleton ownership

A singleton protects only active work. Runlane releases it when the owning run becomes terminal. A conflicting trigger with another idempotency identity fails instead of silently returning unrelated work.

Use a singleton for resources that must not update concurrently. Use queue concurrency when several runs may overlap up to a fixed limit.

Override keys at a trigger boundary

runlane.trigger() and context.trigger() accept key overrides. Use them when the caller owns the request identity. Keep task-level resolvers for rules that apply to every caller.

Do not set different key rules in producers and workers. Register the same task definition everywhere.

Verify the policy

Test:

  1. the same idempotency key returns one run.
  2. a retained owner remains until the expected TTL or reset.
  3. a singleton conflict is visible.
  4. singleton ownership is released at terminal state.
  5. different keys can proceed independently.
  6. invalid resolver output creates no partial run.

Use limit concurrency for shared capacity and prune old runs for retention.

On this page