Job dispatch was designed for batch processing, not phone calls
Callers wait for worker assignment
Every inbound call creates a room, evaluates dispatch rules, queues a job, selects a worker, and initializes an agent process before conversation begins. Ten steps to say hello.
Cold starts punish real callers
LiveKit Cloud shuts down deployed agents after inactivity. Developers report 10 to 20 second cold starts even on paid plans. A caller who waits that long hangs up.
Burst traffic compounds the delay
At 50 concurrent users on the Scale plan, documented p90 latency reached 245 seconds (over four minutes). LiveKit's recommendation: stagger connections with one-second delays.
WebRTC overhead on every call
Each phone call negotiates ICE candidates, STUN discovery, and track subscriptions. These solve NAT traversal for browsers, not for server-side telephony.
Build a Voice AI Agent
fromsignalwire_agentsimportAgentBasefromsignalwire_agents.core.function_resultimportSwaigFunctionResultclassSupportAgent(AgentBase):def__init__(self):super().__init__(name="Support Agent",route="/support")self.prompt_add_section("Instructions",body="You are a customer support agent. ""Greet the caller and resolve their issue.")self.add_language("English","en-US","rime.spore:mistv2")@AgentBase.tool(name="check_order")defcheck_order(self,order_id:str):"""Check the status of a customer order. Args: order_id: The order ID to look up """returnSwaigFunctionResult(f"Order {order_id}: shipped, ETA April 2nd")agent=SupportAgent()agent.run()
import{AgentBase,FunctionResult}from'@signalwire/sdk';constagent=newAgentBase({name:'Support Agent',route:'/support',});agent.promptAddSection('Instructions','You are a customer support agent. Greet the caller and resolve their issue.');agent.addLanguage({name:'English',code:'en-US',voice:'rime.spore:mistv2'});agent.defineTool({name:'check_order',description:'Check the status of a customer order',parameters:{type:'object',properties:{order_id:{type:'string',description:'The order ID to look up'},},required:['order_id'],},handler:(args)=>{returnnewFunctionResult(`Order ${args.order_id}: shipped, ETA April 2nd`);},});agent.run();
packagemainimport("fmt""github.com/signalwire/signalwire-go/pkg/agent""github.com/signalwire/signalwire-go/pkg/swaig")funcmain(){a:=agent.NewAgentBase(agent.WithName("Support Agent"),agent.WithRoute("/support"),)a.PromptAddSection("Instructions","You are a customer support agent. Greet the caller and resolve their issue.")a.AddLanguage(map[string]any{"name":"English","code":"en-US","voice":"rime.spore:mistv2",})a.DefineTool(agent.ToolDefinition{Name:"check_order",Description:"Check the status of a customer order",Parameters:map[string]any{"type":"object","properties":map[string]any{"order_id":map[string]any{"type":"string","description":"The order ID to look up",},},"required":[]string{"order_id"},},Handler:func(argsmap[string]any,rawDatamap[string]any)*swaig.FunctionResult{orderID:=args["order_id"]returnswaig.NewFunctionResult(fmt.Sprintf("Order %v: shipped, ETA April 2nd",orderID),)},})a.Run()}
importcom.signalwire.sdk.agent.AgentBase;importcom.signalwire.sdk.swaig.FunctionResult;importjava.util.List;importjava.util.Map;publicclassSupportAgent{publicstaticvoidmain(String[]args)throwsException{varagent=AgentBase.builder().name("Support Agent").route("/support").build();agent.promptAddSection("Instructions","You are a customer support agent. "+"Greet the caller and resolve their issue.");agent.addLanguage("English","en-US","rime.spore:mistv2");agent.defineTool("check_order","Check the status of a customer order",Map.of("type","object","properties",Map.of("order_id",Map.of("type","string","description","The order ID to look up")),"required",List.of("order_id")),(toolArgs,rawData)->{varorderId=toolArgs.get("order_id");returnnewFunctionResult("Order "+orderId+": shipped, ETA April 2nd");});agent.run();}}
# frozen_string_literal: truerequire'signalwire'agent=SignalWire::AgentBase.new(name:'Support Agent',route:'/support')agent.prompt_add_section('Instructions','You are a customer support agent. Greet the caller and resolve their issue.')agent.add_language(name:'English',code:'en-US',voice:'rime.spore:mistv2')agent.define_tool(name:'check_order',description:'Check the status of a customer order',parameters:{'order_id'=>{'type'=>'string','description'=>'The order ID to look up'}})do|args,_raw|SignalWire::Swaig::FunctionResult.new("Order #{args['order_id']}: shipped, ETA April 2nd")endagent.run
<?phprequire'vendor/autoload.php';useSignalWire\Agent\AgentBase;useSignalWire\SWAIG\FunctionResult;$agent=newAgentBase(['name'=>'Support Agent','route'=>'/support']);$agent->promptAddSection('Instructions','You are a customer support agent. Greet the caller and resolve their issue.');$agent->addLanguage('English','en-US','rime.spore:mistv2');$agent->defineTool(name:'check_order',description:'Check the status of a customer order',parameters:['order_id'=>['type'=>'string','description'=>'The order ID to look up'],],handler:function(array$args):FunctionResult{returnnewFunctionResult("Order {$args['order_id']}: shipped, ETA April 2nd");});$agent->run();
#!/usr/bin/env perlusestrict;usewarnings;uselib'lib';useSignalWire::Agent::AgentBase;useSignalWire::SWAIG::FunctionResult;my$agent=SignalWire::Agent::AgentBase->new(name=>'Support Agent',route=>'/support',);$agent->prompt_add_section('Instructions','You are a customer support agent. Greet the caller and resolve their issue.');$agent->add_language(name=>'English',code=>'en-US',voice=>'rime.spore:mistv2');$agent->define_tool(name=>'check_order',description=>'Check the status of a customer order',parameters=>{order_id=>{type=>'string',description=>'The order ID to look up'},},handler=>sub{my($args,$raw)=@_;returnSignalWire::SWAIG::FunctionResult->new(response=>"Order $args->{order_id}: shipped, ETA April 2nd");},);$agent->run;
#include<signalwire/agent/agent_base.hpp>usingnamespacesignalwire;usingjson=nlohmann::json;classSupportAgent:publicagent::AgentBase{public:SupportAgent():AgentBase("Support Agent","/support"){prompt_add_section("Instructions","You are a customer support agent. ""Greet the caller and resolve their issue.");add_language({"English","en-US","rime.spore:mistv2"});define_tool({.name="check_order",.description="Check the status of a customer order",.parameters={{"order_id",{{"type","string"},{"description","The order ID to look up"}}}},.handler=[](constjson&args,constjson&){autoorder_id=args.value("order_id","unknown");returnswaig::FunctionResult("Order "+order_id+": shipped, ETA April 2nd");}});}};intmain(){SupportAgent().run();}
usingSignalWire.Agent;usingSignalWire.SWAIG;varagent=newAgentBase(newAgentOptions{Name="Support Agent",Route="/support"});agent.PromptAddSection("Instructions","You are a customer support agent. Greet the caller and resolve their issue.");agent.AddLanguage("English","en-US","rime.spore:mistv2");agent.DefineTool("check_order","Check the status of a customer order",new{type="object",properties=new{order_id=new{type="string",description="The order ID to look up"}},required=new[]{"order_id"}},(args,rawData)=>{varorderId=args.TryGetValue("order_id",outvarid)?id:"unknown";returnnewFunctionResult($"Order {orderId}: shipped, ETA April 2nd");});agent.Run();
usesignalwire::agent::AgentBase;usesignalwire::swaig::FunctionResult;useserde_json::json;fnmain(){letmutagent=AgentBase::builder().name("Support Agent").route("/support").build();agent.prompt_add_section("Instructions","You are a customer support agent. Greet the caller and resolve their issue.",&[]).add_language("English","en-US","rime.spore:mistv2");agent.define_tool("check_order","Check the status of a customer order",json!({"type":"object","properties":{"order_id":{"type":"string","description":"The order ID to look up"}},"required":["order_id"]}),Box::new(|args,_raw|{letorder_id=args.get("order_id").and_then(|v|v.as_str()).unwrap_or("unknown");FunctionResult::with_response(&format!("Order {order_id}: shipped, ETA April 2nd"))}),);agent.run();}
Two architectures, one phone call
LiveKit: Job Dispatch
Call arrives, room created
Dispatch rule evaluated
Job created and queued
Worker selected from pool
Worker accepts job
Agent process initialized
Agent joins room
Track subscription established
Audio pipeline ready
Conversation begins
SignalWire: Direct Answer
Call arrives
Routed to the agent
Agent answers
Conversation begins
Documented dispatch latency at 50 concurrent users
Percentile
LiveKit Latency
SignalWire Latency
p50
13,498ms (~13 seconds)
<1200ms
p90
245,331ms (~4 minutes)
<1200ms
p99
276,611ms (~4.6 minutes)
<1200ms
At 50 concurrent users on the Scale plan, a developer documented p90 latency of 245 seconds. LiveKit's response: stagger connections with one-second delays between batches. Source: LiveKit Community Forum.
The solution
Calls as first-class objects, not jobs in a queue
Direct call answering
No job dispatch, no room creation, no track subscription. The agent answers the call. Sub-second response, every time.
One address for everything
Phone numbers, SIP endpoints, AI agents, and subscribers are all addressable the same way. No separate APIs per type.
Native telephony, no bridge
SIP and PSTN handled natively. No WebRTC bridge, no ICE candidates, no STUN discovery, no TURN relay. Media stays on the platform.
Transfers without room migration
Transfer between agents, numbers, and endpoints using standard telephony primitives. No participant migration between rooms.
Architecture comparison
Aspect
LiveKit
SignalWire
Call model
Room with participants
First-class call object
Agent model
Worker dispatched to room
Addressable endpoint
Call answering
Job dispatch (10-50s documented)
Sub-second
Media transport
SIP-to-WebRTC bridge + ICE/STUN/TURN
Native SIP/PSTN
Cold start
10-50 seconds documented
None (HTTP server)
Burst handling
p90 of 245s at 50 concurrent
HTTP endpoint scaling
Transfer
Participant migration between rooms
Standard telephony primitives
Transport is not the bottleneck A published analysis shows the transport layer accounts for less than 5% of total conversational latency. The bottleneck is the model, not the pipe. WebRTC adds complexity without improving voice AI performance.
Get from pip install to production calls
1
Install the SDK
pip install signalwire-agents. One package, one version, zero dependency coordination.
2
Define your agent
Inherit AgentBase, set a prompt, add skills and tools with Python decorators.
3
Point a phone number at it
Configure a SignalWire number to route to your agent. No rooms, no dispatch, no workers.
4
Deploy anywhere
The same code runs on a server, Lambda, Cloud Functions, or Azure. No platform lock-in.
FAQ
Why does LiveKit's dispatch model add so much latency?
LiveKit creates a room, evaluates dispatch rules, queues a job, assigns a worker, and initializes an agent for every call. Each step adds time. Under load, jobs queue behind each other.
Does SignalWire use WebRTC at all?
SignalWire supports WebRTC for browser-based use cases. For phone calls, media flows through the native SIP/PSTN stack with no bridge conversion.
What happens during traffic spikes?
SignalWire agents are HTTP endpoints. They scale the same way any web service scales. No job queue, no worker pool bottleneck.
Can I migrate from LiveKit incrementally?
Yes. SignalWire handles SIP trunking natively. You can route specific numbers to SignalWire agents while keeping other traffic on existing infrastructure.
Trusted by 2,000+ companies
Calls, not jobs. Sub-second, not minutes.
Build voice AI on infrastructure designed for phone calls, not job queues.