Blog
February 15, 2025

Outbound Campaigns at Scale: Architecture Deep Dive

How Trunx processes 10,000-number campaigns with stateless BullMQ workers, Redis semaphores, and automatic DID rotation.

Wei Lin
Wei Lin
4 mins read

Outbound Campaigns at Scale

Outbound dialing is the highest-compute feature in Trunx. A single campaign can target 10,000 numbers, making calls at configurable rates, handling AMD detection, voicemail drops, AI agent handoff, and DID health tracking — all concurrently.

Here's how it works under the hood.

The Job Queue

Campaigns run on BullMQ workers backed by Redis. When you start a campaign, the system enqueues the first batch of dial jobs. Each job is a stateless reactor — it reads campaign state from Postgres, makes a call, writes the result back, and optionally enqueues the next job.

Three worker types handle the lifecycle:

WorkerConcurrencyPurpose
campaign-dialer5Places calls at the configured rate
campaign-health10Processes call results, updates DID health
campaign-stats5Aggregates stats, publishes events

The dialer is self-enqueuing. After each call, it checks if there are more prospects to dial and the campaign is still active. If so, it enqueues the next dial job with a delay based on the configured calls_per_second rate.

SIP Channel Budgets

A shared SIP trunk has finite capacity. Trunx divides it into pools using Redis semaphores:

  • Campaign pool: 60 channels — Outbound dialer
  • IVR pool: 15 channels — Inbound IVR (always reserved)
  • API pool: 8 channels — One-off voice calls

When the campaign pool is full, the dialer automatically slows down. It doesn't fail or drop calls — it just waits for a channel to free up before placing the next call.

This prevents campaigns from starving inbound IVR calls or ad-hoc API calls of trunk capacity.

DID Selection

Each call needs a caller ID. Trunx picks the best DID for each call using a scoring algorithm that considers:

  1. Health score — Higher-health DIDs get priority
  2. Local presence — Match the recipient's area code when possible
  3. Warming schedule — New DIDs get gradually increasing volume
  4. Cooldown status — Cooling DIDs are excluded

The result: your healthiest, most locally-relevant number shows up on caller ID. No manual rotation needed.

AMD and Call Routing

After a call connects, Answering Machine Detection determines whether a human or voicemail picked up:

  • Human answer → Route to AI agent (or play a message, depending on campaign mode)
  • Voicemail → Drop a pre-recorded message and hang up
  • No answer → Mark as no-answer, retry later based on campaign config

AMD runs through the provider layer — either Twilio's cloud-side AMD or Asterisk's local AMD, depending on your active provider configuration.

Failure Modes

At scale, things fail. The system is designed for it:

  • Worker crash — BullMQ retries the job. State is in Postgres, not memory.
  • SIP trunk saturation — Backpressure kicks in. Dialer slows down.
  • DID health drops — Automatic cooldown and rotation. Campaign continues with healthy numbers.
  • Provider outage — Calls fail and get retried. Health events are recorded.
  • Campaign paused — Dialer stops enqueuing. In-flight calls complete normally.

Every failure is an event. Events get published to Redis, logged to the events table, and delivered via SSE to connected clients. You see what's happening in real-time.

Monitoring

While a campaign runs, you get live stats via SSE:

  • Prospects dialed / remaining
  • Connect rate
  • AMD breakdown (human / voicemail / no-answer)
  • Average call duration
  • DID health changes
  • Calls per second (actual vs configured)

The dashboard polls these stats and renders them in real-time. The same data is available via the API for custom monitoring.

Getting Started

Create a campaign, upload prospects, and start dialing:

# Create campaign
curl -X POST https://api.trunx.io/v1/campaigns \
  -H "Authorization: Bearer tk_live_..." \
  -d '{"name": "Q1 Outreach", "mode": "ai_agent", "agentId": "agent_123"}'

# Add prospects
curl -X POST https://api.trunx.io/v1/campaigns/{id}/prospects \
  -d '{"numbers": ["+14155550100", "+14155550101", ...]}'

# Start
curl -X POST https://api.trunx.io/v1/campaigns/{id}/start

For the full technical reference, see the Advanced Campaigns guide.

Wrap-up

Telecom infrastructure shouldn't slow you down. Trunx fits into your workflow — whether you're building voice AI agents, managing outbound campaigns, or scaling SMS at 2am.

If that sounds like the kind of tooling you want to use — try Trunx or join us on Discord.