FreeSWITCH ESL with Node.jsReal-Time Call Control
Build production ESL applications in Node.js — connect to FreeSWITCH Event Socket, subscribe to call events, implement DTMF routing, predictive diallers, and complex call flows with async event-driven architecture.
8021
Default ESL port
500/s
events Node.js handles
async
push-based events
bgapi
non-blocking commands
By Kaushik Parmar · Founder & VoIP Architect, CelloIP Technologies · January 29, 2026 · 16 min read
How do you control FreeSWITCH calls from Node.js using ESL?
Use the modesl npm package to connect to FreeSWITCH's Event Socket Layer (ESL) on port 8021. Subscribe to events like CHANNEL_ANSWER and CHANNEL_HANGUP, and issue commands using conn.bgapi() for non-blocking execution.
Table of Contents
ESL: The Key to Unlocking FreeSWITCH
FreeSWITCH's dialplan XML handles simple routing efficiently. But for dynamic routing — database lookups, real-time agent assignment, custom conferencing, predictive dialling — you need the Event Socket Layer (ESL).
Direct Call Control
Execute FreeSWITCH API commands on live calls in real-time
Event Stream
Push-based events — subscribe to what you need, ignore the rest
Bi-Directional
Send commands while simultaneously receiving events from all channels
Inbound vs Outbound ESL
Inbound ESL
Your app connects to FreeSWITCH port 8021. Subscribe to events from ALL channels globally.
Outbound ESL
FreeSWITCH connects TO your app when a specific call enters the socket() dialplan application. Exclusive control of that call leg.
Outbound ESL: DTMF-Driven Call Routing
FreeSWITCH connects to your Node.js server for each new inbound call. Your server answers, collects DTMF input, and routes based on digit pressed:
const esl = require('modesl');
// FreeSWITCH will connect here for each call (outbound ESL)
const server = new esl.Server({ port: 8084, myevents: true });
server.on('connection::ready', async (conn) => {
const uuid = conn.getInfo().getHeader('Unique-ID');
console.log(`New call: ${uuid}`);
await conn.execute('answer');
await conn.execute('playback', 'ivr/ivr-welcome_to_freeswitch.wav');
await conn.execute(
'play_and_get_digits',
'1 1 3 5000 # ivr/ivr-enter_ext.wav ivr/ivr-invalid.wav DTMF_DIGIT \\d 3000'
);
conn.on('esl::event::CHANNEL_EXECUTE_COMPLETE::*', async (ev) => {
if (ev.getHeader('Application') === 'play_and_get_digits') {
const digit = ev.getHeader('Variable_DTMF_DIGIT');
const dest = digit === '1' ? 'sales_queue'
: digit === '2' ? 'support_queue'
: 'default_queue';
await conn.execute('transfer', dest);
}
});
});<action application="socket" data="127.0.0.1:8084 async full"/> to your FreeSWITCH dialplan extension to connect each call to this server.Inbound ESL: Predictive Dialler
For a predictive dialler, inbound ESL monitors agent availability across all channels. When an agent hangs up, immediately originate the next outbound call:
const esl = require('modesl');
const agentState = new Map(); // agentId → 'idle' | 'busy'
const conn = new esl.Connection('127.0.0.1', 8021, 'ClueCon');
conn.on('esl::ready', () => {
conn.subscribe(['CHANNEL_ANSWER', 'CHANNEL_HANGUP', 'CUSTOM']);
console.log('ESL connected, monitoring all channels');
});
conn.on('esl::event::CHANNEL_ANSWER::*', (ev) => {
const agent = ev.getHeader('Variable_sip_to_user');
if (agent) agentState.set(agent, 'busy');
});
conn.on('esl::event::CHANNEL_HANGUP::*', (ev) => {
const agent = ev.getHeader('Variable_sip_to_user');
if (agent) {
agentState.set(agent, 'idle');
originateNextCall(agent); // dial next lead immediately
}
});
function originateNextCall(agentId) {
const nextLead = getNextLead(); // your DB query
if (!nextLead) return;
const cmd = `originate sofia/gateway/trunk/${nextLead.number}`
+ ` &bridge(user/${agentId})`;
conn.bgapi(cmd); // non-blocking — response via BACKGROUND_JOB event
}
// Auto-reconnect on FreeSWITCH restart
conn.on('error', () => {
setTimeout(() => conn.connect(), 5000);
});Key ESL Event Types
CHANNEL_CREATETrack new calls entering the system
CHANNEL_ANSWERMark call answered, start billing
CHANNEL_HANGUPEnd billing, update agent state
DTMFCapture key presses in IVR
CHANNEL_EXECUTE_COMPLETEKnow when play_and_get_digits finishes
CUSTOM sofia::registerTrack SIP endpoint registrations
BACKGROUND_JOBReceive async bgapi() responses
CHANNEL_BRIDGETrack when two call legs are connected
Production: PM2 + Auto-Reconnect
Production Checklist
module.exports = {
apps: [{
name: 'esl-app',
script: './src/index.js',
instances: 'max',
exec_mode: 'cluster',
max_restarts: 50,
restart_delay: 2000,
env: {
NODE_ENV: 'production',
FS_HOST: '127.0.0.1',
FS_PORT: 8021,
FS_PASS: 'your_secure_password'
}
}]
}Frequently Asked Questions
QWhat is the ESL port by default?
8021. Configure in /etc/freeswitch/autoload_configs/event_socket.conf.xml. Change the default 'ClueCon' password immediately in production.
QCan multiple applications connect to ESL simultaneously?
Yes. Multiple inbound ESL clients can connect to the same FreeSWITCH event socket. Each receives the same event stream independently.
QIs there a Python ESL library?
Yes — python-ESL (C binding) and pure-Python alternatives. Node.js modesl is the most actively maintained as of 2026 with async/await support.
QHow do I handle FreeSWITCH restarts in production?
Use the esl::reconnect event or implement a reconnect loop in your Node.js app. PM2's auto-restart handles process crashes. Store call state in Redis so it survives reconnects.
Related Articles
Official References
Build Your FreeSWITCH Platform
CelloIP Technologies builds production FreeSWITCH platforms — from ESL-powered call centres and predictive diallers to carrier-grade WebRTC switches. Expert configuration and custom development.