Diagnose the problems unique to the voice engine on your own hardware: NIC ownership and reboot cutovers, DNS/tenant provisioning, sealed credentials, and unattached inference.
Most “the self-hosted engine is broken” reports trace back to four things that
work differently on your hardware than on managed cloud:
The engine owns its NIC. There is no ordinary listening socket, and
config changes are reboot cutovers.
DNS and per-tenant provisioning route calls to the right place.
Credentials are sealed at rest, and the engine must be able to unseal
them.
Self-hosted inference must be explicitly attached.
This page is the triage map for all four.
If you did not stand the deployment up yet, start with
On-Prem / Self-Hosted. That page
describes what runs where, and the shape of the install. This page assumes
the system is already deployed and something is wrong.
The engine takes full ownership of its network interface with a
kernel-bypass zero-copy fast path. SIP, RTP, and QUIC all ride one interface
without the kernel’s UDP stack. The most common false alarm: an operator runs
ss -ulpn (or netstat), sees no UDP :443 and no UDP :5060 listener,
and concludes the engine is down.
A missing UDP :443 / UDP :5060 socket in ss is normal and expected
when the engine runs. The fast path steers those packets to the process
below the kernel socket layer, so they never show as bound sockets. Do
not use ss/netstat as your liveness check.
Check liveness the correct way instead:
1
Confirm the process and its interface
Check that the engine process runs, and that the fast-path filter is
attached to the interface. Your deploy runbook names both. If the process
is up and the filter is loaded, the data plane is live, even with no
visible socket.
2
Hit the health endpoint
The control-plane API exposes /health and /healthz. A green health
check plus a live process is the correct “engine is up” signal. A socket
table is not.
3
Place a real probe call
Register a softphone against the tenant SIP domain and dial in, or send a
test INVITE from a trunk. A call that lands and answers is the ground
truth. See
Call not connecting
if the call does not land.
The fast-path filter loads before the engine starts (as a pre-start
step), so a crash-loop cannot black-hole the interface. If you must hand the
NIC back to the kernel (to debug with ordinary tools), stop the engine and
unload the filter. You then see normal sockets again, but no calls flow
until the engine reclaims the interface.
Because the process owns the NIC, a new build or a config change is a reboot
cutover, not a hot reload. If you shipped a change and behavior did not
move, the most likely cause is that the engine host was never rebooted onto
it.
Symptom
Cause
Fix
You shipped a new build, but the old behavior continues
The running process still holds the NIC on the previous image
Reboot the engine host to bring the data plane up on the new build
The engine ignores a trunk / dialplan / agent config edit
NIC-owning modules read owned state at start. Not all settings hot-reload
Reboot. Treat engine-side config as a windowed change
A brief call outage occurs during a deploy
Expected — the interface quiesces while the engine is down
Deploy inside a maintenance window. Drain traffic first
Plan a maintenance window for every engine change. While the engine is
down, the interface is quiesced and in-flight calls drop. This is the
biggest operational difference from a typical service. Budget for it in your
runbook.
Some runtime tunables do reload without a reboot. The dialplan hot-reloads
its rules, and agent timeouts respond to a signal. These values are hydrated
from the config store. But anything that touches the transport, the NIC, or
module load order needs the reboot cutover. When in doubt, reboot.
The engine multiplexes every tenant onto one interface. It tells tenants apart
by the leftmost DNS label of the inbound SIP INVITE, and by SNI on the
QUIC plane. If DNS is wrong, calls never arrive, or they land on the wrong
workspace.
Symptom
Cause
Fix
Carrier INVITEs never reach the engine
The SIP SRV record is missing, or it points at the wrong host
Create the SRV for the tenant SIP domain → engine host. Check with dig SRV
Calls arrive but resolve to the wrong tenant (or none)
The leftmost label is wrong — a <workspace-id>.sip.clutchcall.dev mismatch
Fix the per-tenant CNAME so the label matches the provisioned workspace id
The browser QUIC/WebTransport handshake fails
There is no valid TLS cert for the brand domain, or it expired
Install a valid cert. The transport does SNI across brand domains
The console / relay is unreachable
portal.clutchcall.dev, agent.clutchcall.dev, relay.clutchcall.dev do not point at a host
Point each hostname at the correct box (engine vs control plane)
Onboarding a workspace provisions the DNS records for you. But on a
self-hosted estate, you own the zone. Check that the records resolve:
1
Verify the per-tenant SIP subdomain
Check that <workspace-id>.sip.clutchcall.dev resolves to your engine
host, and that a SIP SRV record routes INVITEs to it. The engine reads
the leftmost label (<workspace-id>) as the tenant. The label must
match the workspace id exactly.
2
Verify the WebRTC / browser subdomain
Check that <workspace-id>.webrtc.clutchcall.dev resolves to the engine
for browser-legged calls.
3
Verify the console, portal, and relay hostnames
portal.clutchcall.dev and agent.clutchcall.dev point at the
control-plane box. relay.clutchcall.dev points at the relay (the same
host for single-site, or the relay mesh for multi-site).
4
Verify TLS for the QUIC plane
The public QUIC/WebTransport listener needs a valid cert for each brand
domain it serves. It selects by SNI. An expired or missing cert looks like
a transport failure, not a DNS failure. Check both.
A wildcard *.clutchcall.dev record proves nothing about a specific label.
Check that the explicitsip/webrtc/SRV records exist. Do not stop
at a wildcard answer. If trunk INVITEs are the problem, cross-check
SIP trunk issues.
Tenant secrets — trunk digest passwords and agent provider API keys — are
sealed at rest. The engine and the control plane share one
key-encryption key (KEK), so the engine can unseal a secret at call time.
There are no plaintext secrets on disk. This makes the deployment HIPAA /
SOC 2 defensible. But if the two sides do not share the same KEK, the
engine holds ciphertext it cannot open.
Symptom
Cause
Fix
Trunk registration / digest auth fails for every tenant
The engine cannot unseal the sealed trunk secret (KEK mismatch)
Align the engine’s KEK with the control plane’s KEK. Reboot the engine
Every agent’s provider call returns 401 / auth error
The sealed provider API key cannot be unsealed
The same KEK mismatch — align the keys, then reboot
Old config works, but newly saved secrets break
The KEK was rotated on one side only
Re-seal secrets under the current KEK on both sides
1
Confirm both hosts share one KEK
The engine host and the control-plane host must have the same
key-encryption key. A mismatch is the number-one cause of “auth fails for
everything at once.”
2
Re-save the affected secrets
If the KEK was rotated, secrets sealed under the old key are unreadable.
Enter the trunk secrets and provider keys again in the console, and save
them. This re-seals them under the current KEK.
3
Reboot the engine
The engine reads the KEK at start (NIC ownership → reboot cutover).
Reboot the engine host after any KEK change, so it picks up the new key.
A KEK mismatch shows as a fleet-wide auth failure (every tenant, every
provider), not as a single bad credential. If exactly one trunk or one agent
fails, suspect that credential, not the KEK.
On a self-hosted engine, you can still use the managed-cloud speech providers
(cloud realtime, ASR/LLM/TTS), if the engine has egress to them. Your own
models on-prem are a separate, explicit step. You must register the model
server with the inference control plane, and the agent’s provider must point
at your endpoint over QUIC. If either step is missing, the agent silently uses
whatever provider its config still names.
Symptom
Cause
Fix
The agent answers, but turns go to a cloud model you meant to replace
The agent provider is still set to the cloud provider
Point the agent at your self-hosted endpoint
The on-prem model is attached, but turns error or time out
The model server is not registered with the inference control plane, or it is unreachable over QUIC
Register the server. Check that the engine can reach it
Cloud providers fail on-prem
Your firewall blocks egress to the provider
Allow egress, or move fully to self-hosted inference
1
Register the model server
Register your GPU-backed model server with the inference control plane, so
the engine can discover it and route to it over QUIC. Full steps are in
Self-Hosted Inference.
2
Point the agent at your endpoint
Set the agent’s provider/endpoint to your on-prem model. Until you do, the
engine uses whatever provider the agent config names — often a cloud one
from an earlier test.
3
Confirm reachability, then place a call
Check that the engine can reach the model host. Then dial in and watch a
turn complete. Use
Latency breakdown
to check that turns hit your GPUs.
Honest status. The QUIC-based inference control plane that discovers and
routes to your GPU pods is shipped. But the fully on-prem speech-to-speech
model path is not yet live-verified end-to-end in a customer deployment.
If you need on-prem GPU inference from day one, plan it as a joint bring-up,
not a drop-in. The managed-cloud model providers work against a self-hosted
engine today if egress is permitted.
If calls connect and sound fine, but reports, call traces, and recordings
never appear, the fault is on the control-plane host, not the engine. The
engine sends every call-lifecycle event, QoS/MOS record, and recording to the
control-plane services. If those services are down or unauthenticated, the
engine produces the data, but the data never lands.
Symptom
Cause
Fix
Reports / call traces stay empty
The event pipeline service on the control-plane host is down
Start the control-plane systemd units. Check that they are healthy
Recordings never show in the console
The recording service or object store is down or misconfigured
Check the recording service and object-store units
Events are silently dropped
The event plane is always TLS + SASL authenticated — there is no plaintext mode
Fix the SASL credentials on both the producer and broker sides
A service does not start at all
There is no Docker — units run against a pinned Node runtime under /opt
Check that the pinned runtime exists and that the unit’s paths are correct
1
Check the control-plane units
The admin API, config database, event pipeline, and object store all run
as systemd units (no containers). Check that each unit is active and
healthy. A stopped unit is the usual cause of empty dashboards.
2
Verify the event plane authentication
The internal event transport is always TLS + SASL authenticated. There is
no plaintext fallback. A credential mismatch drops events silently. Align
the SASL credentials.
3
Confirm the pinned runtime
Services bind to a pinned Node runtime under /opt. If a unit fails to
start, check that the runtime path in the unit file exists on the box.
Some in-console dashboards are still being wired end-to-end on self-hosted
installs. If a specific panel is blank while call audio clearly works, check
that the underlying pipeline is up before you assume a bug. See
Observability overview for what
feeds each panel.
A self-hosted deployment is a single production environment. There is
intentionally no separate sandbox toggle. Do your tests in a dedicated
workspace (or a second install). Never test against a “sandbox” that does
not exist. Treat prod as prod. This prevents a class of “why does my test
call bill / page the on-call” surprises.