Operations
This chapter covers running the gateway day to day: where the logs are, how to read the response-time line, what to monitor, how far one instance goes, what to back up, how to lock it down, and which tuning settings are safe to change.
Logs
The gateway writes to standard output and standard error only. It keeps no log files of its own, apart from logs/performance.csv when LOG_LATENCY=true (the logs/ directory must be writable by the service user).
| How you run it | Follow the log | Rotation |
|---|---|---|
| Binary under pm2 | pm2 logs vexyl-gateway (files live under ~/.pm2/logs/) |
pm2 install pm2-logrotate |
| Binary under systemd | journalctl -u vexyl-gateway -f |
journald rotates by itself |
| Docker | docker logs -f vexyl-gateway |
Set a json-file max-size in the Docker daemon settings or in a logging: block in your compose file |
Lines start with INFO:, WARN:, ERROR: or DEBUG:, and many carry an emoji as well, so grep on the words, not the start of the line.
journalctl -u vexyl-gateway --since "1 hour ago" | grep -E "WARN|ERROR|LOOP-LAG|starvation"
Lines worth knowing:
| Line contains | Meaning | Do this |
|---|---|---|
LATENCY [req-N] |
One line per turn when LOG_LATENCY=true. See Measuring response time. |
Read the columns. |
LOOP-LAG: <n>ms stall ended |
The process was frozen for that long (host CPU steal or a virtual-machine pause). Callers hear broken audio. | Repeated lines are a host problem, not a setting. Move to a quieter host. |
TTS starvation sentence N |
Speech synthesis was slower than playback, so silence was inserted mid-sentence. | Provider slowness or a loaded host. See Tuning that is safe to touch. |
CACHE HIT, CACHE MISS |
TTS cache activity, shown when TTS_CACHE_STATS_LOGGING is on. |
See The TTS cache. |
SIP call connected |
An inbound call reached the bot. | Normal. |
SIP call rejected due to concurrent limit |
Your licence cap was reached; the caller was refused. | See Capacity. |
SIP outbound call rejected |
Same cap, on an outbound call. | Same. |
AUTH: Blocked request from <ip> |
The HTTP whitelist refused a request. | Add the address to HTTP_ALLOWED_IPS if it is legitimate. |
Lifecycle webhook [<event>] failed |
Your webhook endpoint did not accept an event. | See Webhooks and billing. |
STT: transcript held, BARGE-IN: |
Turn-taking decisions. | See Conversation behaviour. |
A punycode deprecation warning at start-up is normal and harmless.
At boot the gateway also logs whether the HTTP whitelist is on: HTTP API IP whitelist enabled or HTTP API IP whitelist disabled (all IPs allowed). Check that line after every configuration change.
Measuring response time
Latency logging is off by default. Switch it on:
LOG_LATENCY=true
Restart the gateway. From then on you get three things: a LATENCY line per turn in the log, a row per turn in logs/performance.csv, and a performance.latency block in the call-ended webhook (see Webhooks and billing).
The line looks like this:
LATENCY [req-N]: buffer: Xms | stt: Xms | llm: Xms | tts: X | send: Xms | response: Xms | total: Xms | txdepth: ...
| Column | Meaning | A large value points at |
|---|---|---|
buffer |
Caller speech plus the wait for the recogniser to decide the caller has finished. | Speech endpointing or a quiet handset. See Short replies and quiet handsets. Not tunable from the gateway side beyond the recogniser's own settings. |
stt |
Recognition tail after the caller stopped. | The speech recogniser or the network path to it. |
llm |
Time for the AI brain to return the first sentence (streaming) or the whole reply. | The brain: prompt size, tool calls, model choice. See When the AI is slow or fails. |
tts |
Synthesis of the sentence that gates playback. Shows the provider; cached sentences are marked. | The voice provider, or very long first sentences. |
send |
Hand-off of audio to the telephony leg. | The host. More than a few tens of milliseconds, or LOOP-LAG lines nearby, means the machine is struggling. |
response |
Transcript ready to first audible audio. This is the number to tune. | Look at whichever of llm, tts and send is largest. |
total |
Caller's first audio to the bot's first audible audio. | Everything above, plus buffer. |
txdepth |
Depth of the outbound audio queue. | Pacing; relevant when audio breaks up. |
To find the slow stage, compare the columns across a handful of turns rather than judging one. A single slow turn is usually the provider; the same column slow on every turn is configuration.
logs/performance.csv has the same columns as the LATENCY line, one row per turn, plus a request id and a timestamp. Open it in a spreadsheet and sort by response.
For quick totals without logging, GET /calls/stats and GET /cache/stats are described in Active calls and stats and TTS cache.
Health checks for monitoring
Poll GET /health every 30 to 60 seconds from your monitoring system. The route is described in Health and licence.
curl -s http://127.0.0.1:8081/health
Alert on:
| Condition | Why it matters |
|---|---|
| No reply, or any HTTP status other than 200 | The gateway is down or hung. |
status is not ok |
The gateway reports itself degraded. |
concurrentCalls.active equal to concurrentCalls.max for several minutes |
Callers are being rejected. |
billing.dead greater than 0 |
Billing records failed every retry. Fix the receiver, then replay (see Billing replay). |
billing.pending growing |
Your billing endpoint is slow or down. |
sipBridge.registered false, when you register to a carrier |
The carrier registration was lost. |
Any circuitBreakers entry in state OPEN for a long time |
A provider is failing and calls are running on fallbacks. |
If concurrentCalls.active is above zero while your PBX shows no calls, sessions are stale. Restart the gateway at a quiet moment. See the troubleshooting chapter (17-troubleshooting.md).
Under Docker the image has a built-in health check, so docker ps shows healthy or unhealthy.
Where sessions are kept
By default, sessions live in memory. They are lost on restart, which also cuts any call in progress. To keep metadata that you posted before a call across restarts, or to share it between several instances, set up Redis:
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
REDIS_PASSWORD=
REDIS_DB=0
SESSION_TTL=3600
SESSION_TTL is in seconds. storage.storage_type in /health shows memory or redis, and storage.redis_connected shows whether the connection is up.
Capacity
One gateway process handles call audio on a single thread, so one instance is bound to about one processor core. On normal hardware the licence cap (concurrentCalls.max in /health) is reached before the processor is. Provider speed matters more to the caller's experience than raw processing power.
To grow beyond one licence or one core:
- Run several instances, each with its own licence, HTTP port, SIP port and RTP port range. The licence is per machine id: instances on one host share a machine id unless they are containers with distinct hostnames, so ask Vexyl how to license them.
- Put them behind your PBX and let the PBX spread calls across them (one trunk or route per instance).
- Keep the instances' RTP ranges apart from each other and from the PBX's own range (see Do not let the RTP ranges overlap).
- Allow for upstream bandwidth: every call sends audio to the PBX or carrier in real time, so a small office uplink can become the limit before the processor does.
- Use Redis (above) if instances must share pre-call metadata.
Watch concurrentCalls.utilization and the "rejected due to concurrent limit" log line to decide when to add an instance.
Upgrades and rollback
The full steps, including how to roll back, are in Upgrading for the binary and Upgrade for Docker. The short checklist:
- Announce a short window to the people who rely on the line.
- Wait until
concurrentCalls.activeis 0. A restart cuts calls in progress. - Verify the checksum of the new download.
- Swap in the new binary or image, keeping the old one for rollback.
- Check
GET /healthand place one test call. - Watch the
LATENCYlines for the first ten minutes and compare them with what you saw before.
Backups
| Path | Back up? | Notes |
|---|---|---|
cdr/pending/ and cdr/dead/ |
Yes, always | Billing records not yet delivered. They are money; never delete them. |
cdr/sent/ |
If you keep delivered records | Exists only when BILLING_CDR_KEEP_DELIVERED=true. |
config/ |
Yes | Your bots, prompts, tools and cost rates. |
.env |
Yes, securely | Holds your provider keys, secrets and licence key. Keep the backup as private as the original. |
cache/tts/ |
No | Rebuildable. Re-warm it with POST /cache/warmup (see The TTS cache). |
logs/ |
Optional | Only the latency CSV lives here. |
Security checklist
The full picture, with what each listener trusts and what data is kept, is in Security.
- Keep the HTTP API on loopback, or restrict it with
HTTP_ALLOWED_IPS. Never expose the HTTP port to the public internet. See Access. - Put the browser WebSocket endpoint behind a TLS proxy, keep its own port on loopback, and set both
WEBSOCKET_AUDIO_API_KEYandWEBSOCKET_AUDIO_ALLOWED_ORIGINS. See Secure it. - Set
SIP_ALLOWED_IPSto the exact addresses of your PBX or carrier. Open the RTP port range in the firewall to those addresses only. - Make
.envreadable by the service user alone (mode 600). - Set
WEBHOOK_SECRETandBILLING_CDR_SECRET, and verify the signature on your receiver. - Leave
WEBHOOK_SSRF_CHECKon unless your receiver is on a private network. - Rotate provider keys when staff who knew them leave.
- The container runs as user id 1000; make mounted directories writable for that user and no one else.
chmod 600 .env
ls -l .env
Tuning that is safe to touch
The defaults suit most installations. Change one setting at a time, restart, and compare LATENCY lines before and after.
| Variable | Default | Move it when |
|---|---|---|
RTP_PACER_LEAD_MS |
350 |
Audio cushion for outbound SIP audio. Raise it (toward 500) if LOOP-LAG lines coincide with audible breaks. Lower it only on a quiet, dedicated host. |
TX_CATCHUP_SLACK_MS |
200 |
Leave it. |
LITEBOT_SENTENCE_PREBUFFER_MS |
600 |
Audio held back before a streamed sentence starts. Raise it if you see TTS starvation warnings. Lower it for a snappier first word on a fast voice provider. |
TTS_TAIL_DELAY_MS |
40 |
Pause after playback before listening resumes. Raise it if the bot hears the tail of its own voice. |
STT_INGRESS_FLUSH_MS |
40 |
Cadence of audio sent to Sarvam streaming. Leave it unless Sarvam support asks. |
STT_INGRESS_MAX_CHUNKS |
2 |
Same; leave it. |
LITEBOT_TTS_SYNTH_CONCURRENCY |
1 |
Keep at 1. Synthesising several sentences at once starves the one that is playing. |
TTS_SENTENCE_MIN_GAP_MS |
200 |
Silence between streamed sentences. See Sentence streaming and seams. |
TTS_SENTENCE_EDGE_FADE_MS |
5 |
Fade at sentence edges. Same section. |
USE_NATIVE_RESAMPLER |
false |
Leave off unless told otherwise. true uses the built-in resampler with automatic fallback; compare runs both so you can validate. Counters appear under resampler in /health. |
NATIVE_RESAMPLER_AUTO_DISABLE_ON_ERROR_RATE |
0.01 |
Error rate (0 to 1) at which the built-in resampler switches itself off. |
MAX_BUFFER_DURATION |
see chapter | Applies to batch recognisers only. See Speech recognition. |
RTP_PACER_LEAD_MS=500