Skip to content

Workflows local binding does not emulate deterministic-ID uniqueness: duplicate create()/createBatch() neither throw nor skip, and can double-execute the workflow #14836

Description

@rarestg

Which Cloudflare product(s) does this pertain to?

Workflows (local development / miniflare emulation), Miniflare, @cloudflare/vitest-pool-workers

What versions are you using?

  • wrangler 4.114.0
  • miniflare 4.20260722.0
  • workerd 1.20260722.1
  • @cloudflare/vitest-pool-workers 0.18.8
  • vitest 4.1.2

What operating system and version are you using?

Linux 6.12 (x86_64)

Describe the bug

The local Workflows binding does not emulate the documented deterministic-ID uniqueness contract, and the divergence is in the dangerous direction: code that relies on the documented production behavior ("duplicate create with the same ID is safely deduplicated") appears to work locally while actually double-executing the workflow body.

Documented production contract (Workers API docs):

  • create({ id }) with an existing ID throws, the existing instance is retained.
  • createBatch([{ id }]) is idempotent: retained IDs are skipped and excluded from the result.

Observed local behavior (vitest-pool-workers harness, versions above):

  1. createBatch([{ id }]) with an existing ID returns length 1 (expected: []).
  2. create({ id }) with an existing ID does not throw.
  3. Duplicate creates can double-execute the workflow: a counter incremented inside the first step.do() body observed the step running twice across duplicate creates (racy — not deterministic run to run).

The cause is visible in the current local binding implementation: create() starts initialization inside waitUntil and suppresses initialization rejections, returning the ID immediately, and createBatch() simply maps every input through create() and returns every result — there is no uniqueness check on either path.

Why this matters

Deterministic instance IDs are the documented mechanism for making at-least-once triggers (e.g. a Queue consumer creating one workflow per message) idempotent. Because the emulator neither throws nor skips nor coalesces execution, local tests actively validate the wrong behavior: a design whose core safety property is "duplicate delivery = free no-op" passes locally and can only be truthfully validated on a deployed environment (remote bindings / wrangler dev --remote being unsupported for Workflows per the local-development known issues).

Reproduction

Minimal test (vitest-pool-workers, a workflow whose first step increments a counter):

import { env, introspectWorkflowInstance } from 'cloudflare:test'
import { expect, it } from 'vitest'

it('deterministic-ID dedup (matches prod docs; fails locally)', async () => {
  const id = 'dedup-repro-1'

  await using instance = await introspectWorkflowInstance(env.MY_WORKFLOW, id)
  await instance.modify(async (m) => {
    await m.disableSleeps()
  })

  const first = await env.MY_WORKFLOW.createBatch([{ id, params: {} }])
  const second = await env.MY_WORKFLOW.createBatch([{ id, params: {} }])

  let createThrew = false
  try {
    await env.MY_WORKFLOW.create({ id, params: {} })
  } catch {
    createThrew = true
  }
  await instance.waitForStatus('complete')

  expect(first.length).toBe(1)
  expect(second.length).toBe(0)   // FAILS locally: returns 1
  expect(createThrew).toBe(true)  // FAILS locally: no throw
  // side-effect counter inside the first step.do() body: observed 2 under
  // duplicate creates (racy), i.e. the body double-executed
})

Expected behavior

Local create()/createBatch() match the documented production semantics (throw / skip-and-exclude), or at minimum coalesce execution per ID so duplicate creates cannot double-run step bodies — plus a documented known-issue note until then, since this is exactly the property idempotent queue-consumer patterns depend on.

I could not find this mismatch in the published known issues or an existing issue here. Happy to provide the full spike harness if useful.

Metadata

Metadata

Assignees

No one assigned

    Labels

    product:workflowsRelating to Cloudflare Workflows: https://developers.cloudflare.com/workflows/

    Type

    No type

    Projects

    Status
    Untriaged

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions