VEXYL AI

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:

  1. Announce a short window to the people who rely on the line.
  2. Wait until concurrentCalls.active is 0. A restart cuts calls in progress.
  3. Verify the checksum of the new download.
  4. Swap in the new binary or image, keeping the old one for rollback.
  5. Check GET /health and place one test call.
  6. Watch the LATENCY lines 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_KEY and WEBSOCKET_AUDIO_ALLOWED_ORIGINS. See Secure it.
  • Set SIP_ALLOWED_IPS to the exact addresses of your PBX or carrier. Open the RTP port range in the firewall to those addresses only.
  • Make .env readable by the service user alone (mode 600).
  • Set WEBHOOK_SECRET and BILLING_CDR_SECRET, and verify the signature on your receiver.
  • Leave WEBHOOK_SSRF_CHECK on 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