Reader setup
Before you evaluate
Use this to set expectations, limits and implementation boundaries before changing anything.
- Read-only database credentials through /etc/vicidial-readonly.cnf (see vicidial-read-only-database-account if you do not have one yet), scoped to the servers table, system_settings row, and log tables you plan to inspect.
- A named owner for the database, dialer, and Asterisk roles who can approve index, replica, and server-configuration changes.
- A one-week baseline of row counts and query time on vicidial_log, vicidial_hopper, and vicidial_live_agents.
- What you will prove
- You will know which role — web, database, dialer/telephony, or archive/reporting — to split out first, what a report slave actually buys you, and what to measure before promising a specific agent count.
- Safety boundary
- This is architecture reasoning, not a migration script. Do not change schema, add a replica, or repoint report pages on a production cluster before testing the change on a copy of the database and getting sign-off from the database owner.
Reader path
How to use this article
- Use it when: You are designing a change and want reliable limits before implementation.
- Expected result: Separate what is known, unknown, and unsafe before you execute.
- Start here: Use it as an evidence review before changing architecture, security, or reporting behavior.
Why one VICIdial box runs out
Fast answer: a single VICIdial box runs Apache with mod_php, MariaDB, and Asterisk together, and all three compete for the same central processing unit (CPU), memory, and disk input/output (I/O) at once. Past a few dozen concurrent agents, the fix is not a bigger box forever — it is splitting the web, database, dialer/telephony, and archive/reporting roles onto separate servers so each role can be sized, monitored, and scaled on its own terms.
In plain language: an agent is the person logged into the browser-based calling screen; a campaign is one outbound or inbound calling project with its own dialing rules and script; a lead is one contact record with a phone number and status; a list is a named batch of leads loaded together and dialed as a unit; the hopper is the shared queue of leads a campaign is currently allowed to call next; a channel is one live audio path Asterisk is holding open, whether it is an agent's phone, an outbound call, or a conference leg; a carrier is the phone company or trunk provider that carries a call onto the public telephone network; and the dial level, or dial ratio, is how many outbound calls the system places for each agent who is free to take one.
Every one of those nouns lives in one shared MariaDB database. VICIdial does not have a separate message queue or event bus — the database itself is the command bus. An agent's browser posts state, a dialer process reads the hopper and writes a manager command row, Asterisk executes it and generates events, and a listener writes the result back. Splitting a cluster means deciding which of those steps can move to its own machine without breaking that loop.
Visual walkthrough
Follow three real demo screens
Captured on an isolated VICIdial demo: Administration screens on September 24, 2026, and the idle Agent screen on August 11, 2026. Each caption states its own capture time, and every sanitized image helps you recognize a related screen; none proves that this article's call, command, or result occurred.Start at Administration home

Use the Administration menu as a map

Confirm version and system-wide context

The four roles that split off a cluster
A cluster is not one thing scaled up — it is four distinct roles, each with a different resource profile, that a single box was quietly doing all at once. The web/agent role serves Admin, the Agent screen, and the roughly one-second browser poll that keeps every logged-in agent's session current. The database role holds the shared configuration, live runtime state, and call history every other role reads and writes. The dialer/telephony role runs the hopper-fill and pacing processes alongside the Asterisk instance that actually places and bridges calls. The archive/reporting role runs the report pages, the VERM analysis modules behind them, and the scheduled jobs that move aged rows out of the live tables.
The installed servers table is where this split becomes configuration, not just concept. Admin lets you mark a server active for web/agent logins, active for Asterisk, active for recording, and separately size its outbound trunk ceiling and its outbound calls-per-second rate. None of those flags are automatic — each one is a real capacity decision you make after you know what that box can actually carry.
Splitting roles does not remove the shared database in the middle. Every role still ends up reading or writing the same tables — vicidial_live_agents, vicidial_hopper, vicidial_manager, vicidial_auto_calls — so the database is the one role that cannot simply be duplicated the same way you duplicate a web server. That asymmetry is the reason the rest of this piece keeps circling back to the database.
role: web/agentservers: 192.0.2.10, 192.0.2.11limited_by: one-second agent poll rate and PHP/Apache worker count, not campaign logic role: databaseservers: 198.51.100.20limited_by: MyISAM table locks, missing composite indexes, and the size of vicidial_log/vicidial_hopper role: dialer/telephonyservers: 198.51.100.31, 198.51.100.32limited_by: CPU and RTP capacity per box plus the outbound trunk channels a carrier actually grants role: archive/reportingservers: 203.0.113.40, 203.0.113.41limited_by: replica lag versus report freshness, and disk growth on log/archive tablesThis sample is a template or reading aid, not a terminal command. There is no output to show.
- Before you run it
- Fill in your own server_id, server_ip, and role flags from the servers table and system_settings before you trust this mapping for your install; the labels are categories, the addresses here are placeholders only.
- Success looks like
- Each role maps to its own machine or pool, and the limiting factor you wrote down matches something you can actually measure — request rate, lock wait time, channel count, or replica lag — not a guess.
- Stop if
- Two roles still share one box, or you cannot name what limits a role beyond it feels slow: measure the limiting factor first, because you are not ready to size a cluster yet.
Why the database is usually first to hurt
The database is first to hurt because it is the only role every other role depends on for every single step. The web tier polls it once a second per agent, the dialer writes a new manager row for every call, Asterisk-side listeners write channel and call state back after every event, and reports scan months of history from the same tables. Nothing else in the architecture has that many concurrent readers and writers landing on the same rows.
Two structural facts make that contention worse than it needs to be. First, the stock schema is overwhelmingly MyISAM, which locks at the table level rather than the row level — a long report scanning vicidial_log or vicidial_closer_log can block the exact inserts and updates the live dialer needs to place the next call. Second, several of the busiest tables ship without the composite index a cluster actually needs: vicidial_hopper indexes only lead_id, vicidial_log indexes only lead_id and call_date, and vicidial_live_agents has no index that leads with campaign_id or status. Every hopper-eligibility check, every live-agent lookup by campaign, and every report filtered by campaign and date falls back to scanning more of the table than it should.
The third pressure is growth with no ceiling. vicidial_log, vicidial_closer_log, recording_log, and the rest of the log-table family only grow, call after call, with nothing built in that ever removes a row on its own. A table that only grows eventually crosses whatever size makes a full scan slow or an index stop fitting comfortably in memory on your specific hardware — there is no single row count or query time this guide can promise you will hit, only the direction of travel. VICIdial ships matching archive tables and archival workers such as ADMIN_archive_log_tables.pl and AST_twoday_tables_control.pl for exactly this reason — the schema assumes you will move history out of the live tables on a schedule, not leave it there.
None of this means every install needs a second database server on day one. It means the database is the first place to look when agents report lag, and the first place to fix — indexes and archival policy — before you spend money adding servers that will still bottleneck on the same slow tables.
SHOW INDEX FROM vicidial_hopper;SHOW INDEX FROM vicidial_log;SHOW INDEX FROM vicidial_live_agents; SELECT table_name, table_rows, ROUND((data_length + index_length) / 1024 / 1024, 1) AS size_mbFROM information_schema.tablesWHERE table_schema = DATABASE() AND table_name IN ('vicidial_log', 'vicidial_hopper', 'vicidial_auto_calls', 'vicidial_manager', 'vicidial_live_agents')ORDER BY size_mb DESC;This sample changes a system, contacts an outside service, needs a live call, or would print real data from a shared server, so it was not run on the demo. Run it only where you are authorized, and compare the result with the success and stop guidance.
- Before you run it
- Run this with a read-only account, for example mysql --defaults-extra-file=/etc/vicidial-readonly.cnf, and repeat it weekly so size_mb becomes a trend line rather than one snapshot.
- Success looks like
- vicidial_hopper, vicidial_log, and vicidial_live_agents each show a composite index that leads with campaign_id or status, and size_mb grows at a rate your archival schedule already accounts for.
- Stop if
- The high-write tables have only their original single-column index and size_mb keeps climbing with no archival job removing rows: fix that before adding a server, because a second database will not undo a missing index or a report that scans a table with no ceiling.
What a report slave is and when you actually need one
A report slave is a second MySQL or MariaDB server that continuously copies data from the primary through standard replication, so reporting queries run against a copy instead of against the same server the dialer is writing to. VICIdial's system_settings table has three fields built for exactly this: slave_db_server names the replica, reports_use_slave_db is a per-report list of which stock report pages should query it, and custom_reports_use_slave_db does the same for custom reports. Nothing routes to a report slave until you fill those in — an unconfigured install simply has the support present with the primary handling every report, which is the normal starting state.
You need a report slave once report traffic is measurably competing with the dialer for the same tables — supervisors running real-time or historical reports during a shift while agents complain the screen is lagging, or a report that used to finish in a couple of seconds now taking most of a minute against a growing vicidial_log. A report slave does not fix a missing index or an ever-growing table; it isolates the damage so a heavy report degrades its own replica instead of the primary the live call flow depends on.
The trade you are making is freshness for isolation. A report slave lags behind the primary by however long replication takes to catch up, which is normally a small number of seconds under healthy load but can grow under the same write pressure that made you want the replica in the first place. Do not point a real-time report, or anything a supervisor uses to decide whether to log an agent out, at the replica — route only the historical and analytical report pages that can tolerate a short delay, using the reports_use_slave_db list to be explicit about which ones.
SELECT slave_db_server, reports_use_slave_db, custom_reports_use_slave_dbFROM system_settings; SHOW SLAVE STATUS\GCaptured demo response · 2026-09-24 22:25 UTC. The displayed command is the command that ran; a safe subset label means it was filtered, redacted, or fixture-scoped. Replays only after you select Replay transcript.
- Before you run it
- Run the first query against the primary through a read-only account; run SHOW SLAVE STATUS on the candidate replica once one is connected and replicating. SHOW SLAVE STATUS needs the REPLICATION CLIENT privilege, which a SELECT-only account such as vicidial-read-only-database-account's does not grant — ask your database owner for that separately rather than widening this account.
- Success looks like
- slave_db_server names a real host, reports_use_slave_db lists only reports that can tolerate a short delay, and Seconds_Behind_Master stays low relative to how fresh those reports need to be.
- Stop if
- An access-denied error on SHOW SLAVE STATUS most likely means your account has SELECT only, not that replication is broken; slave_db_server blank while supervisors already report lag, or Seconds_Behind_Master climbing during normal write load, means finish the configuration or fix the replication bottleneck before anyone relies on the numbers it returns.
How the dialer processes and Asterisk servers distribute load
Scaling the dialer/telephony role means adding Asterisk boxes, not just adding trunks to one box. Each server row carries its own outbound trunk ceiling (max_vicidial_trunks), its own pacing limit (outbound_calls_per_second), and its own Asterisk Manager Interface (AMI) accounts — separate credentials for sending commands, listening for events, and applying updates. Every Asterisk server added to the cluster needs its own manager-send, manager-listen, and auto-dial processes running against that box's own AMI, because those processes are scoped to the server they are paired with.
The hopper and pacing logic are the opposite: they must stay singular across the whole cluster, not per server. The hopper-fill worker refills the shared hopper for a campaign regardless of which Asterisk box will place the call, and the adaptive pacing worker adjusts a campaign's dial level by looking at agent availability across every server the campaign runs on. Running two live copies of a worker that is supposed to be cluster-authoritative double-books the same leads instead of doubling capacity — the same one-writer discipline that governs a single Originate or Redirect command applies to these background workers too.
For campaigns that outrun their own server's trunk supply, VICIdial supports dial-out-only balance servers: a server row can be flagged as a balance-active server with a reserved trunk count that a balance-fill worker draws on to place calls for a busier server's campaign without touching the trunks that server has reserved for its own work. This is a way to add outbound capacity horizontally without adding another full agent-facing Asterisk box.
None of the numbers in these fields — trunk ceilings, calls-per-second, reserved trunks — are ones the application can respect for you. They only work if you measure a box's real CPU and Real-time Transport Protocol (RTP) audio-channel headroom under your actual codec mix and conferencing engine, then set the field to match, instead of leaving the installer default in place.
- Per-server: manager-send, manager-listen, and auto-dial processes, one live set per Asterisk box.
- Cluster-wide: the hopper-fill and adaptive-pacing workers, exactly one live instance per campaign across the whole cluster.
- Balance/dial-out-only servers add outbound trunk capacity without adding agent-facing channels.
#!/bin/bashset -euo pipefail CNF=/etc/vicidial-readonly.cnf mysql --defaults-extra-file="$CNF" -e "SELECT active_asterisk_server, max_vicidial_trunks, outbound_calls_per_second, vicidial_balance_active, balance_trunks_offlimits FROM servers WHERE active = 'Y';" asterisk -rx 'core show channels count' Captured demo response · 2026-09-24 22:25 UTC. The displayed command is the command that ran; a safe subset label means it was filtered, redacted, or fixture-scoped. Replays only after you select Replay transcript.
- Before you run it
- Run the SQL half from the web/report tier's read-only account against the primary; run the Asterisk half locally on each candidate dialer/Asterisk box you are about to add to the cluster.
- Success looks like
- Every active row has a real max_vicidial_trunks and outbound_calls_per_second you set after measuring that box's CPU and RTP headroom, not the installer default, and the channel count stays comfortably under that ceiling during your busiest hour.
- Stop if
- A row's trunk ceiling still shows the installer default, or the channel count is already brushing the ceiling: fix that server's row or add capacity before pointing more agents or campaigns at that box.
Sizing reasoning for 500 agents, without inventing a benchmark
There is no published number that safely tells you how many agents one web server, one database server, or one Asterisk box can carry, because that number depends on your hardware, your campaign mix, your codec, and your conferencing engine. What you can do is reason from what each role is actually doing and then measure your own install against that reasoning.
On the web/agent tier, the Agent screen's browser polling is documented as roughly a one-second refresh. Five hundred logged-in agents polling once a second is a floor of about 500 requests per second against the Agent state endpoint alone, before manager actions, dispositions, lead searches, or supervisor report traffic are added. That is arithmetic from a documented interval, not a capacity claim — it tells you the request rate you must load-test against, not the number of web servers you need.
On the database tier, the limiting resource is usually not raw CPU but lock wait time and I/O — how long a write to vicidial_live_agents or vicidial_auto_calls waits behind a table lock, and how many rows a query has to scan because an index is missing. Measure query latency and lock wait time directly rather than estimating agent capacity from memory size alone.
On the dialer/telephony tier, the limiting resource is channel capacity: how many simultaneous RTP audio paths a given Asterisk box's CPU and network can sustain at your codec, plus how many outbound trunk channels your carrier actually grants. That ceiling belongs in the server row's trunk and pacing fields, set from a measured test on that specific hardware, not copied from another deployment's number.
Put together, sizing 500 or more agents is an exercise in measuring four separate ceilings — request rate, lock wait time, channel capacity, and report I/O — on your own hardware, then adding capacity to whichever ceiling you hit first. It is not a single number you can look up once and trust afterward, because the campaign mix, disposition pattern, and report schedule that produced last quarter's ceiling will not be the same ones straining the cluster next quarter.
The operational cost of running a cluster instead of a box
Every role split out is a role you now have to patch, monitor, and recover on its own schedule. A single box fails as one unit and comes back as one unit; a cluster fails one role at a time, and a monitoring plan that only watches the database server misses a dialer box quietly running out of trunk channels or a web server slowly falling behind on agent polling.
High availability for the database is the part most often assumed rather than designed. VICIdial's stock architecture has no automatic database failover; running a multi-node replicated cluster behind a virtual IP (VIP) address managed by a load balancer is a reasonable direction for closing that gap, not a drop-in fix — it still has to be tested against the same MEMORY-table volatility and single-writer command-bus behavior described earlier, on a copy of your database, before you trust it with live agents.
Adding Asterisk servers multiplies the AMI accounts and connections you manage — each server row carries its own send, listen, and update credentials, and the Asterisk Manager Interface has its own configured connection ceiling on every box, one that a growing set of listener and update workers can approach faster than expected on a busy cluster. Track active AMI connections per Asterisk box the same way you track trunk channels, and confirm your own configured ceiling rather than assuming a number.
Recordings and archived history add their own operational weight once they live on a dedicated role: recordings stored only on the local disk of the server that captured them are a data-loss risk if that server is lost, and archive/report tables still need the same backup and restore discipline as the live tables they were moved out of. A cluster is worth the added weight once one box is the reason agents are waiting on the screen — it is not worth adopting piece by piece just because a guide says a 500-agent operation should have one.
- Monitor each role's own limiting resource, not only the database server.
- Test any database high-availability design against MEMORY-table and single-writer behavior before trusting it with live agents.
- Track Asterisk Manager Interface connection counts per server as you add listener and update workers.
Evidence ledger
Verification basis
- VICIdial coordinates web, database, Asterisk, and recording roles through one shared MariaDB instance that also acts as the inter-process command bus — observed directly in this lab; report-slave support (system_settings.slave_db_server) ships present but unconfigured by default.
- Constraint source: reasoning from the shipped mechanism, not a named benchmark — MyISAM's table-level locking, the missing composite indexes on vicidial_hopper, vicidial_log, and vicidial_live_agents (confirmed directly against this schema with the sample above), unbounded log-table growth with no automatic row removal, and the absence of automatic database failover in VICIdial's stock architecture.
Primary references
Sources
- VICIdial official wikiVICIdial Group · accessed August 5, 2026
- MariaDB standard replicationMariaDB Foundation · accessed August 5, 2026