SipFlow: Call Signaling, Recordings, and Cluster Deployment
Debugging SIP calls usually means running tcpdump, downloading PCAP files, and opening Wireshark. It works, but it’s slow, requires server access, and doesn’t scale.
SipFlow is RustPBX’s built-in signaling capture system. It records every SIP message and RTP packet, stores them efficiently, and presents a visual timeline in the web console. Need a recording? Generate a WAV from the captured RTP data with one click. Need to debug a call from last week? Query it by call ID.
And when a single node isn’t enough, SipFlow supports cluster deployment with consistent hashing.
How SipFlow Captures Data
RustPBX’s SIP proxy mirrors every SIP message and RTP packet to the SipFlow subsystem in real time. This happens inside the process – no port mirroring, no external taps.
Each captured packet includes:
- Timestamp (microsecond precision)
- Source and destination IP:port
- Message type (SIP or RTP)
- Full payload (SIP headers + body, or RTP header + audio)
Packets are encoded in a compact binary format (magic 0x5346) and written to local storage.
Local Storage
By default, SipFlow stores data locally. Configure it in config.toml:
[sipflow]
type = "local"
root = "/data/sipflow"
subdirs = "hourly" # none, daily, or hourly
flush_count = 500 # Batch write every 500 packets
flush_interval_secs = 10 # Or every 10 seconds
Storage Structure
With subdirs = "Hourly", SipFlow creates a directory per hour:
/data/sipflow/
2026/
06/
01/
00/
sipflow.db # SQLite index (call_meta, sip_msgs, media_msgs)
data.raw # ZSTD-compressed binary packets
01/
sipflow.db
data.raw
...
The SQLite database maps call IDs to packet offsets. The raw data file is ZSTD-compressed for efficient disk usage.
Disk Usage Estimate
| Calls/Day | SIP Storage | RTP Storage | Total (Hourly) |
|---|---|---|---|
| 1,000 | ~50 MB | ~200 MB | ~250 MB |
| 10,000 | ~500 MB | ~2 GB | ~2.5 GB |
| 100,000 | ~5 GB | ~20 GB | ~25 GB |
SIP-only capture is much lighter. If you don’t need RTP, you can disable media capture and keep storage minimal.
Viewing SIP Flows in the Console
From any Call Record, click the SipFlow tab to see the signaling timeline:

The timeline shows every SIP message in order:
- INVITE, 100 Trying, 180 Ringing, 200 OK, ACK
- BYE, 200 OK
- Any error responses (403, 404, 408, 486, 503)
- Message details: headers, SDP, timestamps
This is invaluable for debugging:
- Why did a call fail? Check the final response code and reason header
- Was the SDP negotiated correctly? Compare offer and answer
- Did the carrier reject the call? Look at the 403/503 from the trunk side
No SSH, no tcpdump, no Wireshark. Just open the console and look.
Generating Recordings from RTP
If RTP capture is enabled, you can generate a WAV recording on demand:
- Open the call record
- Click Download WAV
- SipFlow reads the RTP packets, decodes the audio, and generates a WAV file

This works even for calls where file-based recording wasn’t enabled. SipFlow’s RTP capture is independent of the recording policy.
The media stats view also shows per-leg packet counts, helping you diagnose one-way audio or packet loss issues.
Cluster Deployment
When you run multiple RustPBX nodes, each node captures its own traffic. To query any call from a central location, deploy SipFlow in cluster mode.
Architecture
┌──────────────┐
│ RustPBX │
│ Node 1 │──── UDP 3000 ────┐
└──────────────┘ │
▼
┌──────────────┐ ┌──────────────┐
│ RustPBX │ │ SipFlow │
│ Node 2 │──── UDP ─│ Standalone │
└──────────────┘ 3000 │ Server │
└──────────────┘
┌──────────────┐ │
│ RustPBX │ │ HTTP API
│ Node 3 │──── UDP ──│ port 3001
└──────────────┘ 3000 │
└──────────────┘
The standalone SipFlow server:
- Receives packets from all nodes via UDP (port 3000)
- Stores them in the same hourly structure
- Exposes an HTTP API (port 3001) for queries:
/flow?callid=xxx,/media?callid=xxx
Consistent Hashing
For high-volume deployments, run multiple SipFlow servers. Each packet is routed to a specific server based on a consistent hash of the call_id. This ensures all messages for a given call land on the same server.
When a node is added or removed, only 1/N of calls are redistributed – the rest stay put.
Configuring RustPBX for Remote SipFlow
[sipflow]
type = "remote"
timeout_secs = 5
[[sipflow.nodes]]
udp = "10.0.1.50:3000"
http = "http://10.0.1.50:3001"
[[sipflow.nodes]]
udp = "10.0.1.51:3000"
http = "http://10.0.1.51:3001"
The console transparently queries the right node when you view a call’s SipFlow data.
Deploying the Standalone Binary
cargo build --bin sipflow --release
/static/blog/assets/sipflow --udp 0.0.0.0:3000 --http 0.0.0.0:3001 --dir /data/sipflow
Or via Docker. The binary is lightweight – it only handles packet ingestion and queries, no SIP proxy overhead.
Upload to External Storage
Both local and remote modes support uploading to S3 or an HTTP endpoint for long-term retention:
[sipflow.upload]
type = "s3"
vendor = "aws"
bucket = "my-sipflow-archive"
region = "us-east-1"
access_key = "your-access-key"
secret_key = "your-secret-key"
endpoint = "https://s3.us-east-1.amazonaws.com"
root = "sipflow/"
This runs after each hourly rotation, so your local disk stays clean.
Summary
| Feature | Benefit |
|---|---|
| SIP capture | Debug any call from the console, no server access needed |
| RTP capture | Generate recordings on demand, independent of recording policy |
| Visual timeline | See full SIP flow with headers and SDP |
| Cluster mode | Scale across multiple nodes with consistent hashing |
| External upload | Archive to S3 for long-term storage |
What’s Next?
So far we’ve covered internal communication, carrier connectivity, privacy numbers, and operations. The final piece? Running a full contact center – queues, agents, skill-based routing, supervisor monitoring, and more. That’s the CC addon, and it’s the topic of our last post.