5.4 KEMI tradeoffs — when it wins, when cfg wins¶
[!IMPORTANT] KEMI is not always the right choice, and "use KEMI" is not the answer to "my cfg is getting complicated." Sometimes it is; sometimes the right move is to push complexity into a database, a module, or an external service instead. This chapter is the decision framework.
The cost model, in numbers and in shape¶
The native cfg path executes a pre-compiled AST. Per-message overhead from the script itself is dominated by the cost of the function calls into module C code — typically nanoseconds per call. A trivial cfg request_route (sanity, lookup, relay) costs single-digit microseconds total on commodity hardware.
The KEMI path adds:
- One interpreter-call entry at dispatch time. ~0.5–2 µs in Lua, ~5–10 µs in Python.
- One glue marshalling step per KSR.* call. ~0.5–2 µs in Lua, ~1–5 µs in Python.
- Whatever the interpreter does on its own — string allocations, table lookups, GC pressure.
For a route that calls KSR.* five times and does a couple of conditionals, the script-side overhead is somewhere between 5 µs (Lua) and 50 µs (Python) on top of the cfg path's couple of µs. Whether that matters depends entirely on your throughput target and your routing complexity. At 1 k CPS, 50 µs per call is 50 ms of CPU per second — nothing. At 50 k CPS, that's 2.5 seconds of CPU per second — you need more cores or a faster path.
[!TIP] The honest rule: measure. Drive realistic load (chapter 2.5) against both implementations and compare. KEMI's overhead is workload-dependent in ways that can't be predicted from the cfg complexity alone.
When KEMI is clearly the right answer¶
You need real data structures. Per-call decision tables, multi-level routing graphs, anything you'd naturally model as a hash-of-arrays-of-strings. cfg can do flat dispatcher tables and htable lookups; it can't do "for each entry in this dynamic list, check these three conditions."
You need to call an HTTP API and parse a JSON response. It is possible to do this in cfg with http_async_client plus json module, but the script-side ergonomics are awful: you end up writing pseudo-regex against the response and managing async callbacks with named cfg routes. In KEMI:
ok, resp, _, _ = KSR.http_client.curl_obj("https://billing.internal/check", json.dumps({"call_id": call_id}))
if ok != 1:
KSR.sl.send_reply(503, "Billing unavailable")
return KSR.x.exit()
result = json.loads(resp)
if not result["allowed"]:
KSR.sl.send_reply(403, "Call not authorised")
return KSR.x.exit()
That's 8 lines. The cfg equivalent is a multi-route mess with named async resumption points.
Logic that changes weekly. If your routing rules are business-driven and change often, KEMI's faster development loop (script reload via kamcmd, no Kamailio restart) is worth the per-call cost on its own.
You're integrating with non-SIP systems. Auth against an internal API, fraud scoring with a Redis-backed model, custom CDR pipelines — KEMI gives you the libraries and the testing tools (unit tests for your Lua/Python files) that cfg cannot.
When cfg wins¶
Hot-path proxying with simple logic. A stateless or low-state proxy that just routes based on dispatcher tables: cfg, no question. KEMI's per-call overhead at this complexity is pure waste.
Registrar servers. REGISTER handling is dominated by usrloc and auth_db calls, both of which are equally fast from cfg or KEMI. The cfg version is shorter and clearer.
Anything where the routing is genuinely a small decision tree. If your request_route is 50 lines of if-elif-else against is_method, $ru =~ pattern, and module function calls, it's already the right shape for cfg.
Performance-critical sub-routes. Even in a KEMI-driven deployment, you can keep the hottest sub-route in cfg and call it from the script via KSR.cfg.route_inv("hot_path"). This is the hybrid pattern and is often the right answer at scale.
The hybrid pattern¶
In a large deployment, the practical structure is rarely "all cfg" or "all KEMI." It's:
flowchart TD
Recv["receive_msg()"] --> CfgEntry["cfg request_route<br/>(thin shell)"]
CfgEntry --> Sanity["cfg route('sanity')<br/>fast checks"]
Sanity --> Decide{simple or complex?}
Decide -- simple --> CfgRoute["cfg route('simple_relay')<br/>direct t_relay"]
Decide -- complex --> Script["dispatch to KEMI<br/>ksr_request_route()"]
Script --> ExtCall["HTTP / DB / business logic"]
ExtCall --> CfgRoute2["KSR.cfg.route_inv('relay')<br/>back to cfg"]
CfgRoute --> Forward["forward / t_relay"]
CfgRoute2 --> Forward
classDef cfg fill:#1f6feb,stroke:#1f6feb,color:#fff
classDef script fill:#bf8700,stroke:#bf8700,color:#fff
classDef ext fill:#238636,stroke:#238636,color:#fff
class CfgEntry,Sanity,CfgRoute,CfgRoute2 cfg
class Script script
class ExtCall ext
- cfg owns the fast-path sanity and routing.
- KEMI owns the business logic: authentication, authorisation against external systems, fraud scoring, anything that wants real data structures.
- cfg owns the forwarding decision and the relay.
- Both can call into each other; the bridge is bidirectional.
The cost of this is one bridge-crossing per complex call. At ~5 µs per crossing in Lua, this is essentially free even at high CPS — and you keep cfg's speed for the 90% of routing that doesn't need a script.
Choosing a language, in one paragraph¶
If you have no other constraint: Lua. It has the most complete KEMI binding coverage, the lowest per-call overhead, and is small enough that the interpreter footprint per worker is negligible. Python if your team's expertise is overwhelmingly Python and the per-call cost (5x Lua's typically) is acceptable for your throughput — usually true. JavaScript if you're trying to share business logic between Kamailio and a JS-heavy backend. Ruby if you have a specific reason to want it. Don't choose based on language preference alone if you're at the very high end of throughput; the gap between Lua and Python matters at 10 k+ CPS.
What you should not use KEMI for¶
A few anti-patterns worth flagging:
- Don't move performance-critical logic into KEMI "because it's easier to write." Easier to write does not mean better in a system that runs at 10 k CPS. Profile first, decide second.
- Don't use Lua globals as a cross-worker cache. They aren't shared. Use
htable(covered later in chapter 8.3) or a database. - Don't replace short cfg routes with longer KEMI equivalents for ergonomics alone. A clear 20-line cfg
if-elifis usually better than a clever 60-line Python class. - Don't try to wrap every cfg primitive in a KEMI helper. If you find yourself rewriting
is_method()in Python because "the cfg version is awkward," you're fighting the architecture. Just callKSR.is_method("INVITE").
The handbook moves next into state — how tm and dialog keep transactions and calls alive across messages, which is what the routing engine and KEMI both ultimately interact with.
← Table of contents · ← 5.3 KEMI lifecycle · Next: 6.1 Transactions (tm) →