i am facing race condition for workers in prod env
# ask-ai
b
i am facing race condition for workers in prod env
t
Thanks for reaching out, Utkarsh. I'm an AI assistant and I'm here to help. Give me a moment while I research this topic.
Thanks — I hear you’re seeing a race condition with workers in production. I can help, but I need a few details to diagnose and suggest fixes. Please share: 1. Which worker system/framework are you using (e.g., LiveKit Agents, a custom worker pool, Sidekiq, Celery, etc.)? 2. What observable symptoms indicate the race (duplicate processing, lost updates, inconsistent state, errors/timeouts)? 3. Any relevant logs or error messages (timestamps and worker IDs helpful). 4. Are jobs queued centrally (Redis, RabbitMQ, database)? If so, which queue driver and dispatch logic do you use? 5. How many worker instances/processes are running and how are they scaled? 6. Any recent code or deployment changes before the issue started? If you prefer, paste a short log snippet and the job/dispatch code that’s causing the problem and I’ll point to likely causes and concrete fixes (e.g., idempotency, locking, single-writer patterns, per-resource queues). Sources: examples/survey | livekit/livekit_composite | DeepWiki | Avoiding Room ID Reuse When Re-Creating Rooms With the Same Name | server-sdk-ruby | livekit/livekit_composite | DeepWiki
b
below is the situation:
Copy code
production environment has multiple LiveKit agent worker processes running (PIDs: 1650, 1713, 1779). When the scheduler dispatches a job, both workers accept it simultaneously:

Timeline (from logs):
05:37:01.657Z → Worker 1: Creates SIP participant (SCL_7rCnTibuHbtD)
05:37:01.664Z → Worker 2: Creates SIP participant (SCL_sjc6DnLxHMc8) [7ms later!]
                Same identity: sip_target_7091517137
                Same room: collection-call-3148067097691554843-1767332220

05:37:01.668Z → LiveKit Server: "DUPLICATE_IDENTITY detected"
              → Terminates first call
              → Worker 1 gets TwirpError(code=canceled, status=408)
At
2026-01-02T05:37:01.657Z
, two SIP participants are being created simultaneously for the same identity (
sip_target_7091517137
) in the same room (
collection-call-3148067097691554843-1767332220
): 1. SCL_7rCnTibuHbtD - First call created 2. SCL_sjc6DnLxHMc8 - Duplicate call created ~7ms later What Happens:
Copy code
2026-01-02T05:37:01.668Z - received leave request for SCL_7rCnTibuHbtD
2026-01-02T05:37:01.668Z - server initiated leave with reason: DUPLICATE_IDENTITY
The LiveKit server correctly detects and terminates the first call due to duplicate identity, while the second one continues. this is my worker: cli.run_app( WorkerOptions( entrypoint_fnc=entrypoint, prewarm_fnc=prewarm, agent_name="outbound-caller", port=8001, prometheus_port=PROMETHEUS_PORT, )
t
Thanks — this is clear and helpful. You're seeing two agents accept the same job and both call LiveKit to create a SIP participant with the same identity, which triggers LiveKit's DUPLICATE_IDENTITY behavior and terminates the first participant. What you can do (short, prioritized steps): 1. Use a room-level metadata lock (recommended) • Before creating the SIP participant, attempt to set a metadata key like
sip_call_initiated_{phone_number}
to the identity. If the key already exists, abort — another worker is handling it. • Only proceed to
create_sip_participant
if you successfully set the metadata. Clear the metadata on failure if appropriate. 1. Make creation idempotent • Compute a deterministic participant identity (you already have
sip_target_7091517137
) and ensure your logic treats repeated attempts as no-ops when a participant with that identity already exists. 1. Optional external locking • If metadata locking isn't suitable, use an external distributed lock (Redis Redlock, etc.) keyed by the phone number or identity, acquire the lock, create the participant, then release. 1. Job-acceptance filtering • If your agent framework lets you inspect job attributes before accepting, only accept jobs you will execute (e.g., route by shard or a job attribute) to reduce simultaneous accepts. Concrete next step I can help with • I can provide a ready-to-use code snippet for your agent (Python) that implements the metadata lock flow you outlined. Do you want the snippet for the exact worker code you pasted (using the same API calls), or for a different language/lock mechanism (Redis)? Sources: SIP Lifecycle Management Agent | LiveKit docs | Company directory phone assistant | LiveKit docs | Agents Framework | livekit/livekit_composite | DeepWiki | server-sdk-go | livekit/livekit_composite | DeepWiki | Agents (Python) | livekit/livekit_composite | DeepWiki
b
adding
job_type=JobType.JT_ROOM
to your
WorkerOptions
. This tells LiveKit to ensure only ONE worker per room, preventing the race condition. is this true?
t
Yes — setting
job_type=JobType.JT_ROOM
on your
WorkerOptions
ensures LiveKit dispatches at most one agent instance per room, which prevents multiple workers from accepting the same room-level job and avoids the duplicate-participant race you observed. A couple short caveats: • This prevents simultaneous agents for the same room, but you should still keep idempotent creation/guards (metadata lock or external lock) for defense-in-depth. • Ensure your jobs are truly room-scoped;
JT_ROOM
applies to jobs dispatched with room context. Would you like the exact code change to add to your
WorkerOptions
or a small snippet showing a metadata-lock fallback? Sources: An open source stack for real-time multimodal AI | Agents Framework | livekit/livekit_composite | DeepWiki
b
can you show me an example where the field job_type=JobType.JT_ROOM is used in the worker option?
t
Thanks — I’ll show a concise example. Below are two minimal snippets (Python and pseudo-Python for your existing style) demonstrating
job_type=JobType.JT_ROOM
in
WorkerOptions
. Python (using WorkerOptions):
Copy code
from livekit.agents import cli, WorkerOptions, JobType

def entrypoint(ctx):
    # your job logic here
    pass

if __name__ == "__main__":
    cli.run_app(
        WorkerOptions(
            entrypoint_fnc=entrypoint,
            job_type=JobType.JT_ROOM,
            agent_name="outbound-caller",
            port=8001,
        )
    )
If you prefer the same style as your original snippet:
Copy code
cli.run_app(
    WorkerOptions(
        entrypoint_fnc=entrypoint,
        prewarm_fnc=prewarm,
        agent_name="outbound-caller",
        job_type=JobType.JT_ROOM,
        port=8001,
        prometheus_port=PROMETHEUS_PORT,
    )
)
This ensures LiveKit dispatches a single agent instance per room, preventing multiple workers from being assigned the same room and avoiding the duplicate-participant race. I still recommend keeping an idempotency/metadata-lock as a safety net; want a snippet that adds the metadata-lock check as well? Sources: OpenAI and LiveKit partner to turn Advanced Voice into an API | OpenAI and LiveKit partner to turn Advanced Voice into an API | OpenAI and LiveKit partner to turn Advanced Voice into an API
b
i do not see any option in worker for job_type, what is your source for this info
t
Good catch — you're right to question that. The
job_type
field does not exist on
WorkerOptions
. Use
worker_type
(e.g.,
WorkerType.ROOM
) to register an agent as a room-scoped worker so LiveKit will dispatch at most one agent per room. Example:
Copy code
from livekit.agents import cli, WorkerOptions, WorkerType

def entrypoint(ctx):
    ...

cli.run_app(
    WorkerOptions(
        entrypoint_fnc=entrypoint,
        worker_type=WorkerType.ROOM,
        agent_name="outbound-caller",
        port=8001,
    )
)
If you still see duplicate accepts after switching to
WorkerType.ROOM
, keep a metadata-lock or idempotent check as a safety net. Do you want a ready-to-use metadata-lock snippet for your agent? Sources: OpenAI and LiveKit partner to turn Advanced Voice into an API | Agents (Node.js) | livekit/livekit_composite | DeepWiki | Building an Automated IVR Menu Caller | LiveKit docs | Agents (Python) | livekit/livekit_composite | DeepWiki
b
• Multiple worker processes (PIDs 170, 227) are running in parallel • Both pick up the same job simultaneously • LiveKit prevents duplicate SIP participant creation, causing one to fail # PID 170 (fails) Failed to create SIP participant: TwirpError(code=canceled, message=twirp error unknown: canceled, status=408) # PID 227 (succeeds) ✅ SIP participant created with identity: sip_target_7091517137 in UAT the same code is working fine where i have only 1 pod but in prod i have 2 pods running and the isssue is caused
t
Thanks — I see the problem: two pod processes accept the same job concurrently in prod (PID 170 fails with Twirp canceled; PID 227 succeeds). That matches a race where LiveKit prevents duplicate identities. What to do next (prioritized): 1) Ensure worker is room-scoped • Set
worker_type=WorkerType.ROOM
in your WorkerOptions so LiveKit dispatches a single agent per room:
Copy code
cli.run_app(
    WorkerOptions(
        entrypoint_fnc=entrypoint,
        prewarm_fnc=prewarm,
        worker_type=WorkerType.ROOM,
        agent_name="outbound-caller",
        port=8001,
    )
)
This prevents multiple agents being assigned the same room. If you already have this set, tell me and we’ll investigate dispatch behavior. 2) Add a metadata-based lock for defense-in-depth (recommended) • Before calling
create_sip_participant
, attempt to atomically set a room metadata key (e.g.
sip_call_initiated_{phone}
) to your identity. If the key already exists, abort. Clear the key on failure/cleanup. • This handles edge cases and any transient dispatch races. 3) Make creation idempotent • Use the deterministic identity (
sip_target_...
) and treat a “participant already exists” response as success/no-op instead of retrying. 4) Alternative: distributed lock • If metadata locking isn’t suitable, use an external lock (Redis Redlock) keyed by phone/identity around the creation step. 5) Logging & retries • Log which worker acquired the lock / metadata was set. • On Twirp canceled/408, avoid blind retry — check room state/participant list first. Quick diagnostics I need if it still fails: • Confirm whether you’ve set
worker_type=WorkerType.ROOM
in prod. • Paste a short snippet of the job-accept logic (where you call
job_request.accept()
and then create the SIP participant). • Share a log showing job acceptance timestamps from both PIDs around a failure. Which would you like first — a ready metadata-lock code snippet for your agent, or help confirming your worker registration and dispatch behavior in prod?
b
i think by default it is set to worker_type=WorkerType.ROOM,?
t
You're right to check —
worker_type
is not set to
WorkerType.ROOM
by default. You must explicitly set it in
WorkerOptions
. What to do now: • Ensure your worker is started with
worker_type=WorkerType.ROOM
so the LiveKit orchestrator dispatches a single agent per room:
Copy code
cli.run_app(
    WorkerOptions(
        entrypoint_fnc=entrypoint,
        prewarm_fnc=prewarm,
        worker_type=WorkerType.ROOM,
        agent_name="outbound-caller",
        port=8001,
    )
)
• If you're already setting that in prod, confirm all pods use the same
agent_name
and
worker_type
, and check logs for multiple
job_request.accept()
calls for the same
job.id
. Defense-in-depth (recommended): • Add a room-metadata lock or external distributed lock around
create_sip_participant
so concurrent accepts cannot both create the same identity. Tell me whether: • You currently set
worker_type=WorkerType.ROOM
in prod (yes/no), and • You want a metadata-lock code snippet for your agent. Sources: OpenAI and LiveKit partner to turn Advanced Voice into an API | OpenAI and LiveKit partner to turn Advanced Voice into an API | OpenAI and LiveKit partner to turn Advanced Voice into an API | An open source stack for real-time multimodal AI
b
{"message": "received job request", "level": "INFO", "name": "livekit.agents", "job_id": "AJ_Kifx3sVbmXdq", "dispatch_id": "AD_aNSaMn6fPFFt", "room": "collection-call-3148067097691554843-1767334920", "room_id": "RM_439DvAVRvvUU", "agent_name": "outbound-caller", "resuming": false, "enable_recording": false, "timestamp": "2026-01-02T06:22:00.225276+00:00"}
{"message": "received job request", "level": "INFO", "name": "livekit.agents", "job_id": "AJ_C63bSYZv7Bgv", "dispatch_id": "AD_YKkAthi6vZwT", "room": "collection-call-3148067097691554844-1767334920", "room_id": "RM_QzkMc9GeRLq6", "agent_name": "outbound-caller", "resuming": false, "enable_recording": false, "timestamp": "2026-01-02T06:22:00.231039+00:00"}
{"message": "received job request", "level": "INFO", "name": "livekit.agents", "job_id": "AJ_zhtJ8kA7yPfy", "dispatch_id": "AD_qKfYJf7dEkAx", "room": "collection-call-12328838394994950050-1767337560", "room_id": "RM_ET5nj8EcTHuG", "agent_name": "outbound-caller", "resuming": false, "enable_recording": false, "timestamp": "2026-01-02T07:06:00.224495+00:00"}
{"message": "received job request", "level": "INFO", "name": "livekit.agents", "job_id": "AJ_UfDx7QadU77N", "dispatch_id": "AD_WjTcWBDRo8uz", "room": "collection-call-12328838394994950050-1767337560", "room_id": "RM_ET5nj8EcTHuG", "agent_name": "outbound-caller", "resuming": false, "enable_recording": false, "timestamp": "2026-01-02T07:06:00.225018+00:00"}
{"message": "received job request", "level": "INFO", "name": "livekit.agents", "job_id": "AJ_HxUVZQz4A4Sh", "dispatch_id": "AD_MttYEWWpS2Pb", "room": "collection-call-12328838394994950050-1767340620", "room_id": "RM_Bv4mgSjFpV2c", "agent_name": "outbound-caller", "resuming": false, "enable_recording": false, "timestamp": "2026-01-02T07:57:00.221104+00:00"}
{"message": "received job request", "level": "INFO", "name": "livekit.agents", "job_id": "AJ_TAPfWQ6qwtrb", "dispatch_id": "AD_riuq6EFRMuRo", "room": "collection-call-12328838394994950050-1767340620", "room_id": "RM_Bv4mgSjFpV2c", "agent_name": "outbound-caller", "resuming": false, "enable_recording": false, "timestamp": "2026-01-02T07:57:00.322851+00:00"}
t
Thanks — those logs are helpful. They show multiple job requests for the same room being received (same room name repeated with very close timestamps), which explains why two pod processes can both proceed and cause the duplicate-identity conflict. Quick summary and next steps: • Your logs show multiple job dispatches for the same room arriving at different workers. This happens when
worker_type
is not explicitly set to
WorkerType.ROOM
or when dispatch/accept coordination still allows simultaneous accepts. • Ensure every pod starts with this exact setting in prod:
Copy code
cli.run_app(
    WorkerOptions(
        entrypoint_fnc=entrypoint,
        prewarm_fnc=prewarm,
        worker_type=WorkerType.ROOM,
        agent_name="outbound-caller",
        port=8001,
    )
)
• After you confirm that, redeploy and check whether duplicate
job_request.accept()
logs disappear. Defense-in-depth (recommended if duplicates persist): • Add a room-metadata lock before
create_sip_participant
(atomic
update_room_metadata
to set
sip_call_initiated_{phone}
— abort if already set). • Or use an external distributed lock (Redis) keyed by room/phone. Tell me: 1) Do all prod pods currently include
worker_type=WorkerType.ROOM
? (yes/no) 2) If yes, paste a short log snippet showing both pods'
job_request.accept()
calls for the same `job_id`/`room` and I’ll suggest the exact metadata-lock placement and code.