Chandra PrakashCo-Founder & CTO
Case Study / web-autodialer
Subject
Open source (MIT)

Web Autodialer — open-source FreeSWITCH dialer & PBX

Building an outbound autodialer on FreeSWITCH — concurrency control, DNC enforcement, and a FreePBX-style admin panel. MIT licensed.

FreeSWITCHFastAPICeleryPostgreSQLMIT

What we built

Web Autodialer is an outbound dialer that imports a CSV contact list and places calls through FreeSWITCH, honouring a strict simultaneous-call limit, with a real-time dashboard over WebSocket. It started as a narrow MVP brief — dial a list, cap concurrency, show progress — and grew into a full telephony platform with a FreePBX-style admin panel, a visual call-flow builder, live diagnostics and reporting.

It is open source under MIT: github.com/Zedtreeo/web-autodialer

The architecture

| Layer | Tech | |---|---| | API | Python 3.11 + FastAPI | | Task queue | Celery | | Broker | RabbitMQ | | Telephony | FreeSWITCH + Event Socket (ESL) | | Database | PostgreSQL | | Real-time | Redis counters + WebSocket | | Frontend | Single-page HTML + vanilla JS | | Infra | Docker Compose |

The browser talks to FastAPI, which enqueues work onto RabbitMQ. Celery workers originate calls over ESL. Call state flows back as FreeSWITCH events, lands in Postgres, and live counters sit in Redis so the dashboard can poll or stream without hammering the database.

The part that is actually hard

Everything interesting in a dialer is concurrency control. The naive version — loop the contact list, fire originate per row — falls apart immediately: you exceed what the trunk will carry, the carrier starts rejecting, and you cannot tell a real "no answer" from congestion you caused yourself.

The cap has to be enforced against live channel state, not against how many tasks you dispatched. A queued task is not a call; an answered call is not a completed one. The design keeps the authoritative count in Redis, decrements on hangup events from ESL rather than on task exit, and refuses to originate past the ceiling even when workers are idle and eager. Concurrency is configurable from 1 to 100 per campaign, plus a separate calls-per-second ceiling, because trunk providers rate-limit on both axes and they fail differently.

Everything else — pause/resume/stop, scheduling via Celery Beat, retry handling — is bookkeeping on top of getting that one invariant right.

Compliance is a feature, not a footnote

The dialer enforces a DNC / opt-out suppression list at two points: on import, so suppressed numbers never enter a campaign, and again at dial time, so a number added to the list mid-campaign stops being called. Doing it only at import is the common shortcut and it is wrong — campaigns run for days, and suppression requests arrive while they run.

Outbound calling is regulated on consent, opt-out handling, calling hours, and robocall rules, and the specifics vary by jurisdiction. The software gives you the enforcement points; the obligation to configure them correctly is the operator's.

The PBX panel

Twelve modules — SIP trunks, extensions, ring groups, IVR menus, a visual call-flow builder, queues via mod_callcenter, conferences, inbound routes, time conditions, voicemail, music-on-hold, and a raw dialplan editor. Each one generates FreeSWITCH XML and reloads it over ESL, so the panel is a front-end to real config rather than a parallel source of truth that drifts.

Diagnostics covers active channels, extension registration state, config-file inspection, and a read-only debug console with allowlisted commands and SIP-trace toggles.

Security posture — read this before deploying

The application ships with no authentication, and the Diagnostics section includes an in-app terminal, a database console, and a file editor. On a public IP with no login, that combination is effectively an open root shell.

Bind it to 127.0.0.1 and reach it over an SSH tunnel, or put it behind a VPN or an authenticating reverse proxy, and add real login before any public deployment. This is stated plainly in the repository README, and it is not a theoretical concern — it is the single most important thing to get right about running this software.

What I would do differently

Auth should have been in the first commit. Deferring it made the MVP faster and made every deployment afterwards a manual security exercise. Retrofitting authentication across twelve admin modules is meaningfully harder than starting with it.

The vanilla-JS frontend was the right call and I would keep it. No build step, no framework churn, trivially auditable — for an operations panel that a single admin uses, this beats a SPA framework.

Redis as the concurrency authority needs a reconciliation pass. If a worker dies between originate and the hangup event, the counter can drift high and the campaign throttles itself. A periodic sweep against actual FreeSWITCH channel state fixes it; a restart is the current workaround.

Related