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):
createBatch([{ id }]) with an existing ID returns length 1 (expected: []).
create({ id }) with an existing ID does not throw.
- 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.
Which Cloudflare product(s) does this pertain to?
Workflows (local development / miniflare emulation), Miniflare,
@cloudflare/vitest-pool-workersWhat versions are you using?
4.114.04.20260722.01.20260722.1@cloudflare/vitest-pool-workers0.18.84.1.2What 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):
createBatch([{ id }])with an existing ID returns length 1 (expected:[]).create({ id })with an existing ID does not throw.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 insidewaitUntiland suppresses initialization rejections, returning the ID immediately, andcreateBatch()simply maps every input throughcreate()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 --remotebeing unsupported for Workflows per the local-development known issues).Reproduction
Minimal test (vitest-pool-workers, a workflow whose first step increments a counter):
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.