Skip to main content
Version: 0.9.13

Ops service operations

The Ops service runs infrastructure management and background processing.

  • Scaler — autoscales API and Voice instances based on utilization metrics. Provider-agnostic via per-provider shell scripts (scaleUp.sh, scaleDown.sh, scalingStatus.sh).
  • Tasker — runs scheduled and queued background tasks (DB backups, maintenance, recording processing, email notifications) on a Redis-backed job queue.

Both components use Redis-based leader election with separate keys so only one active leader processes at a time.

Instance

FieldValue
NetworkPrivate only — no public IP
ScalableNo — single instance

Containers

ContainerBasePurpose
voiceai-scalerNode 24-alpineDistributed scaling orchestrator
voiceai-taskerNode 24-alpineBackground job + cron runner
voiceai-db-migrateNode 24-alpineOne-shot Prisma migration and release seeding container
voiceai-otel-collectorotel/opentelemetry-collector-contrib:0.150.1Telemetry collector

Neither the Scaler nor the Tasker exposes an HTTP health endpoint. Their health is determined by container status and log output.

Scaler decision loop

  1. Every SCALING_EVALUATION_INTERVAL_MS (default 30s), the leader reads utilization metrics from Redis against scaleUpThreshold / scaleDownThreshold + min/max from ServerGroup.scalingConfig in Postgres.
  2. On scale up, fetch optional bootstrap secrets from Secrets Manager (secretsName), generate a cloud-init via generate-cloud-init.sh, run scaleUp.sh.
  3. Poll scalingStatus.sh until ready, then wait for a service heartbeat in Redis (~5 min for cloud-init + container startup).
  4. Voice: append to the Kamailio dispatcher set in Redis. API: managed LB picks up via labels.
  5. Cooldown — gates the next decision.

Scale-down reverses the flow: remove from routing, drain active calls (Voice waits up to 60 min), delete the server, clean Redis + Postgres state.

Tasker work model

The Tasker is two cooperating loops over Postgres tables — TaskerCron for schedules and TaskerJob for the queue:

  • Scheduler (leader only). Every SCHEDULER_POLL_INTERVAL_MS (default 10s) the leader reads enabled TaskerCron rows; for any whose nextRunAt is due it inserts a TaskerJob and advances nextRunAt from the cron expression. Because only the leader schedules, each due run is enqueued exactly once across the fleet.
  • Worker (every instance). Every WORKER_POLL_INTERVAL_MS (default 5s) each instance claims up to WORKER_CONCURRENCY (default 5) pending jobs with SELECT … FOR UPDATE SKIP LOCKED, so a job is never picked up twice. Failures retry with exponential backoff + jitter up to maxAttempts, then move to DEAD. Jobs stuck RUNNING longer than 5 minutes (e.g. a crashed instance) are recovered to PENDING.

Jobs reach the queue two ways: the scheduler creates them from cron rows, or another service enqueues them directly in response to an event (call hangup, registration, Stripe webhook, scaling event, …).

Tasker jobs and handlers

Each TaskerJob.type maps to exactly one handler. The handlers that ship in the Tasker:

Scheduled by default

These TaskerCron rows are seeded when the deployment is provisioned. Billing and registration schedules are filtered out at provisioning time when their flag is off (see below).

Job typeWhat it doesDefault scheduleFeature flag
BACKUP_DATABASEpg_dump of the platform DB uploaded to S3 (S3_BUCKET / S3_PREFIX), pruned to retentionDays.Every 6h (0 */6 * * *)
BILLING_RETRY_FAILED_OVERAGE_PAYMENTSRetry Stripe charges for subscriptions with failed overage invoices.Every 6h (0 */6 * * *)subscriptionManagement
BILLING_EXPIRE_GRACE_PERIODSCancel paused subscriptions past the 30-day grace period and release their numbers.Daily 03:00 (0 3 * * *)subscriptionManagement
BILLING_SETTLE_MONTHLY_OVERAGESBill accumulated overages for semiannual / yearly subscriptions.Monthly, 1st 00:00 (0 0 1 * *)subscriptionManagement
BILLING_CLEANUP_CANCELLED_TEAMSSoft-delete teams whose subscription was cancelled past the retention period.Daily 04:00 (0 4 * * *)subscriptionManagement
CLEANUP_ABANDONED_REGISTRATIONSDelete PendingRegistration rows never completed within retentionDays.Daily 04:00 (0 4 * * *)registration
TASKER_CLEANUP_JOBSDelete completed / dead TaskerJob rows older than retention (default 180 days).Daily 03:00 (0 3 * * *)
SCORING_CONVERSATIONSBatch-score conversations not yet scored. Seeded disabled — enable only if you batch-score.Every 10 min (*/10 * * * *)qaScoring

Event-driven

Enqueued directly by other services as work happens — no cron row.

Job typeWhat it doesEnqueued byFeature flag
EMAIL_SENDSend a transactional email via SMTP or SES.Flows, Platform settings test sender
NOTIFICATION_SENDRender a DB notification template and deliver it.Auth, registration, Stripe webhooks, Scaler, usage alerts, password-expiry
SMS_SENDSend an SMS via Vonage.Voice flow stepssms
AUDIO_RECORDING_PROCESSStitch, transcode and upload call-recording segments to the team's S3 bucket.TelPhi recording uploader
PERSIST_CONVERSATION_SPAN_TREEPersist the SignOz span tree for a finished conversation.TelPhi at call end
PERSIST_CONVERSATION_SIP_LADDERPersist the SIP ladder for a finished conversation.TelPhi at call end
SCORING_CONVERSATIONSScore a single conversation right after hangup (when a transcript exists).TelPhi flow engineqaScoring
BILLING_SUBSCRIPTION_ROLLOVERRoll a subscription into its next billing period.Billing eventssubscriptionManagement
BILLING_CALCULATE_OVERAGESCompute usage overages for a billing period.Billing eventssubscriptionManagement

Available but not seeded

Handlers that exist but have no default schedule. Add a schedule from the Tasker admin in TelWeb (see Cron schedules) or trigger them ad-hoc.

Job typeWhat it doesFeature flag
TWILIO_RECONCILE_NUMBERSCompare DB phone numbers against the Twilio account; optional autoFix.
REDIS_DB_CONSISTENCY_CHECKReconcile Redis routing / state against Postgres; optional autoFix.
PASSWORD_EXPIRY_CHECKWarn users whose password nears the PASSWORD_MAX_AGE_DAYS limit (default 90); enqueues NOTIFICATION_SEND with a link built from NEXTAUTH_URL.
TTS_MEDIA_CACHE_CLEANUPDelete expired TtsMediaCache rows and their files from the Media service.ttsMediaCache

How feature flags gate Tasker work

Flags reach the Tasker in two layers:

  1. Provisioning-time filter. The cron seeder drops the billing schedules (BILLING_*) when subscriptionManagement is off, and the registration-cleanup schedule when registration is off — those TaskerCron rows are never created.
  2. Handler-time guard. Some handlers re-check a flag before doing work, so even a manually enqueued job safely no-ops when the feature is disabled:
    • SCORING_CONVERSATIONS requires qaScoring, and additionally honours per-plan / per-team / per-app QA toggles — a conversation that fails any check is marked SKIPPED.
    • SMS_SEND is only enqueued upstream when sms is on.
    • TTS_MEDIA_CACHE_CLEANUP targets the Media service cache, which only exists when ttsMediaCache is on.

Flag keys and their FEATURE_* env mapping are in Feature flags.

See also