2.5 Sizing & tuning for traffic patterns¶
[!IMPORTANT] No two Kamailio deployments need the same numbers. A 100k-user registrar and a 100k-call/sec stateless proxy can run on the same hardware but with completely different
children,-m,-M, and kernel-side tuning. This chapter is about reading your traffic and choosing accordingly — not about a magic recipe.
The four chapters before this one explain how Kamailio works internally. This one is about turning that understanding into operational defaults: how many workers, how much memory, what kernel knobs, and how those decisions shift per traffic pattern.
The dimensions you actually tune¶
flowchart TB
subgraph CPU["CPU"]
Workers["children<br/>tcp_children"]
Cores["physical cores"]
end
subgraph Memory["Memory"]
Shm["-m (shm pool)"]
Pkg["-M (pkg per worker)"]
TotalRAM["total RSS"]
end
subgraph Network["Network"]
SocketBuf["rmem_max · wmem_max"]
NICq["NIC ring buffers"]
FD["file descriptors<br/>(TCP/WS scale)"]
end
subgraph App["Application"]
Hash["hash_size (tm/dialog)"]
Timers["timer intervals"]
DBpool["DB pool size"]
end
classDef cpu fill:#1f6feb20,stroke:#1f6feb
classDef mem fill:#bf870020,stroke:#bf8700
classDef net fill:#23863620,stroke:#238636
classDef app fill:#6e768120,stroke:#6e7681
class CPU cpu
class Memory mem
class Network net
class App app
Each of these knobs maps to something in the architecture chapters: children to the process model, -m/-M to the memory architecture, hash_size to the per-bucket concurrency pattern. The right number for each depends on which knob is your bottleneck — and the only way to know is to look.
Starting baselines (then measure)¶
Treat the numbers below as defaults you start from and walk away from once telemetry shows you what's really going on.
| Knob | Default | Bump when… |
|---|---|---|
children=8 (UDP workers per listener) |
8 | UDP recvfrom queue is backed up under load |
tcp_children=4 |
4 | TCP/WS connection count grows; reads stall |
-M 8 (MB pkg per worker) |
8 | script logs out of memory on parsing |
-m 64 (MB total shm) |
64 | kamcmd core.shmmem shows free < 30% at peak |
hash_size=1024 for tm, dialog, htable, usrloc |
1024 | concurrent calls / users far exceed this and lock contention shows in perf |
tcp_max_connections=2048 |
2048 | WebSocket / TCP-heavy workloads |
tcp_connection_lifetime=120 (sec) |
120 | want longer idle keepalive |
Pattern 1 — Stateless proxy¶
Shape of traffic. High request rate. Small messages. Mostly INVITE/REGISTER getting forwarded after a routing decision. Optionally retransmission-handling via tm but no per-call state held.
What dominates the cost. - CPU — every packet parses, runs the route, forwards. - Network packet rate (PPS), not bandwidth. - Almost zero shm growth per call.
Sizing.
- children = 2 × cores, up to ~32. Stateless workers spend most of their time in the route, not blocked, so more workers translates almost directly to more PPS.
- -M 8 (default fine — no transactions held).
- -m 64–128 is plenty unless you also run modules like dispatcher with large tables.
- Tune kernel UDP buffers: net.core.rmem_max = 16777216, net.core.wmem_max = 16777216. With high PPS, the default 256 KB UDP receive buffer is the most common cause of "drops we can't explain."
What kills you.
- recvfrom drops at the kernel level if the worker pool can't keep up — increase children and/or kernel buffers.
- Slow DNS lookups inside the route: even one bad upstream lookup can park a worker for seconds. Use cache modules or pre-resolved IPs.
Pattern 2 — Registrar server¶
Shape of traffic. Mostly REGISTER with steady refresh cadence (typically every 30–600 s). Modest in-flight transaction count but a huge static state: every active contact lives in usrloc.
What dominates the cost.
- shm size — usrloc cache scales linearly with online contacts. Plan ~500 B per contact as a rough heuristic.
- Database writes if you've configured usrloc with synchronous DB mode (avoid in scale; use db_mode=1 with periodic flushes).
- Spike load during re-registration storms — when an upstream peer fails, every device may re-REGISTER within seconds.
Sizing.
- children = cores, maybe a bit less. Workers spend less time per message than a stateless proxy.
- -m sized for peak_contacts × 600 B × 2 (the 2× is headroom for fragmentation and tm transactions in flight). For 100 k contacts → ~120 MB → use -m 256 or higher.
- usrloc hash_size should be at least peak_contacts / 10 so buckets stay short — for 100 k contacts, hash_size=16384 is reasonable.
- Set usrloc db_mode=1 (write-back) and flush periodically rather than per-REGISTER.
What kills you.
- shm exhaustion when a DB sync fails and the in-memory cache grows unchecked — alert on kamcmd ul.dump count vs DB row count divergence.
- Re-registration storms after upstream failover — consider rate-limiting via pike or randomised registration intervals on UAC config.
Pattern 3 — Stateful proxy / call routing¶
Shape of traffic. INVITE/BYE flows with full transactions tracked by tm, often dialog to follow calls end-to-end. Per-call state lives in shm from INVITE until either the call ends or the dialog timer expires.
What dominates the cost.
- shm per concurrent call — tm + dialog together cost roughly 5–10 KB per call. Plan accordingly.
- Transaction hash contention if hash_size is too low for the call rate.
- Database lookups on call setup (auth, accounting).
Sizing.
- children = 2 × cores is a reasonable start. Calls touch more code than stateless forwarding, so per-message CPU is higher.
- -m for peak_concurrent_calls × 10 KB × 2. 10 k concurrent calls → ~200 MB → use -m 512 or 1024.
- tm hash_size = 4096 or higher for high call rates. The default 1024 hits per-bucket contention around 50 k transactions/sec.
- Database pool size (per-module DB connections × number of workers) should be sized so you don't queue: at least children connections in the pool.
What kills you.
- Long-running routes due to slow DB queries — use timeouts and async modules.
- shm fragmentation when concurrent call count fluctuates wildly — monitor largest_free block, not just free.
Pattern 4 — WebSocket / WebRTC gateway¶
Shape of traffic. Tens of thousands of long-lived TCP/TLS connections, modest per-connection message rate, paired with RTPEngine for media.
What dominates the cost. - File descriptors — every connection is one FD plus kernel-side TCP state. - TCP main and tcp_children scheduling — TCP worker count limits how fast you can drain busy connections. - TLS handshake CPU during connection storms.
Sizing.
- tcp_children = 2 × cores if WS is the dominant traffic. WS reads need a worker per connection burst.
- tcp_max_connections set to the peak connection count + headroom.
- ulimit -n must be set above tcp_max_connections + children + DB pool + slack. 65535 is the common floor.
- tls_max_connections mirror tcp_max_connections if all are TLS.
- Increase tcp_connection_lifetime if your UAs send WS keepalives less frequently than the default 2 minutes.
- For high TLS handshake rates, pin TLS workers to specific cores and enable session resumption.
What kills you.
- EMFILE: too many open files — first thing to check is ulimit -n and tcp_max_connections.
- TCP main becoming the bottleneck if connection accept rate is huge; this is rare but happens with WebRTC sign-up storms.
How to measure, not guess¶
Three commands you should be running before you change any sizing knob:
kamcmd core.shmmem # total / free / largest_free / fragments
kamcmd core.pkgmem all # per-worker pkg state
kamcmd tm.stats # transactions in flight, per-bucket depth
Plus from the OS side:
ss -lntu # listen socket queue depths
ss -tan state established | wc -l # active TCP connections
perf top -p $(pgrep -f kamailio | head -1) # where the workers are spending CPU
[!TIP] The single most useful operational metric is
largest_freeincore.shmmem. Totalfreecan stay healthy whilelargest_freecollapses due to fragmentation. Whenlargest_freedrops below the size of a typical shm allocation, the next big alloc fails even though "there's plenty of memory left." Alert on this, not onfree.
Tuning workflow¶
- Set defaults. Start from the table at the top of this chapter. Don't over-engineer the first deployment.
- Drive realistic load. Use SIPp or capture-replay from production. Synthetic load patterns mislead.
- Look at four things. CPU (
top/perf), shm (kamcmd core.shmmem), pkg (kamcmd core.pkgmem all), and message rate at the listener (ss -lntuor per-listener counters). - Change one knob at a time. If you double
childrenand add 1 GB of shm and bumphash_sizesimultaneously, you can't tell which fixed the problem. - Re-baseline after every meaningful traffic-shape change. A registrar with 10 k users and a registrar with 100 k users want different numbers, even with everything else identical.
The kernel-side tuning that pays off across all patterns is small: increase UDP buffer sizes for high PPS, increase ulimit -n for TCP-heavy workloads, and consider net.core.netdev_max_backlog on hosts that see traffic bursts. Everything else is workload-specific.
← Table of contents · ← 2.4 Lifecycle · Next: 3.1 Reception →