boundless-author-7811
01/02/2026, 6:03 AMtall-airline-16756
01/02/2026, 6:03 AMtall-airline-16756
01/02/2026, 6:03 AMboundless-author-7811
01/02/2026, 6:10 AMproduction 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:
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,
)tall-airline-16756
01/02/2026, 6:10 AMsip_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 | DeepWikiboundless-author-7811
01/02/2026, 6:12 AMjob_type=JobType.JT_ROOM to your WorkerOptions. This tells LiveKit to ensure only ONE worker per room, preventing the race condition. is this true?tall-airline-16756
01/02/2026, 6:12 AMjob_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 | DeepWikiboundless-author-7811
01/02/2026, 6:13 AMtall-airline-16756
01/02/2026, 6:13 AMjob_type=JobType.JT_ROOM in WorkerOptions.
Python (using WorkerOptions):
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:
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 APIboundless-author-7811
01/02/2026, 6:15 AMtall-airline-16756
01/02/2026, 6:15 AMjob_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:
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 | DeepWikiboundless-author-7811
01/02/2026, 8:01 AMtall-airline-16756
01/02/2026, 8:01 AMworker_type=WorkerType.ROOM in your WorkerOptions so LiveKit dispatches a single agent per room:
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?boundless-author-7811
01/02/2026, 8:04 AMtall-airline-16756
01/02/2026, 8:05 AMworker_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:
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 AIboundless-author-7811
01/02/2026, 8:19 AM{"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"}tall-airline-16756
01/02/2026, 8:19 AMworker_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:
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.