MRCP Ports and Channels

This article is the authoritative reference for how the Capacity Private Cloud MRCP-API allocates network ports, how that allocation relates to the MEDIA_SERVER__NUM_CHANNELS setting, how the choice of Docker network_mode changes what you need to configure, and how port usage differs once TLS (SIPS, MRCPS) and SRTP are enabled. Use it as customer-facing reference material when sizing, configuring, or troubleshooting MRCP-API deployments.

Summary

  • The MRCP-API allocates ports dynamically as calls arrive. It does not reserve the full port range at startup.
  • MEDIA_SERVER__NUM_CHANNELS sets the maximum number of simultaneously active channels; it is an upper bound, not a target.
  • Each active channel uses one TCP MRCP control port, plus an RTP/RTCP port pair (2 UDP ports). The RTP pairing holds whether the media is plain RTP or SRTP.
  • Both the MRCP TCP range and the RTP UDP range are sized at 2 × NUM_CHANNELS ports. RTP/RTCP fills its range with paired allocation (even=RTP, odd=RTCP). MRCP uses one socket per channel but draws from the full 2 × NUM_CHANNELS range — listen ports for new sessions can appear anywhere in the reserved range, so all 2 × NUM_CHANNELS ports must be available even though only NUM_CHANNELS are bound at any one moment.
  • Plain SIP signalling uses a small fixed number of listening sockets regardless of load. SIPS (SIP over TLS) keeps one long-lived TLS connection per active call — it scales 1:1 with concurrency.
  • MEDIA_SERVER__NUM_CHANNELS and the configured port ranges (MEDIA_SERVER__MRCP_PORT_BASE, MEDIA_SERVER__RTP_PORT_BASE, and the ports: block in docker-compose.yml) must be consistent with each other — and the RTP range must be sized for 2 × NUM_CHANNELS, not one port per channel.
  • When network_mode: host is set, the ports: block in docker-compose.yml is redundant and should be commented out or removed.

The port ranges

The MRCP-API listens on several distinct ranges of host ports:

PurposeProtocolDefault range / port
SIP signallingTCP and UDP5060
SIPS (SIP over TLS)TCP5061
RTSPTCP554
MRCP control channelsTCPMRCP_PORT_BASEMRCP_PORT_BASE + 2 × NUM_CHANNELS − 1
RTP / SRTP media + RTCPUDPRTP_PORT_BASERTP_PORT_BASE + 2 × NUM_CHANNELS − 1
  • MEDIA_SERVER__MRCP_PORT_BASE defaults to 20000.
  • MEDIA_SERVER__RTP_PORT_BASE defaults to 25000.
  • SIP, SIPS and RTSP listeners do not scale with NUM_CHANNELS.

Why both ranges are sized at 2 × NUM_CHANNELS, and why they fill differently

Both the MRCP and RTP port ranges are reserved at the same width — 2 × NUM_CHANNELS ports each. They fill very differently in practice:

RTP / RTCP — the range is reserved AND fills. Every active call needs two adjacent UDP ports: an even-numbered RTP port for media and the next odd port for the corresponding RTCP control flow. This pairing comes from RFC 3550 and is unchanged by SRTP — SRTP simply encrypts the payloads of the RTP and RTCP packets while still using the same pair of UDP ports. A deployment configured with NUM_CHANNELS=200 therefore actively uses up to 400 UDP ports in the RTP range (200 RTP + 200 RTCP). With the default RTP_PORT_BASE=25000, the in-use ports span 25000–25399.

MRCP — the range is reserved and listen ports can appear anywhere in it. Each call uses one TCP socket for its MRCPv2 control channel — there is no equivalent of RTCP for control signalling, so no second port per channel is needed. At any single moment only NUM_CHANNELS sockets are bound (one per active call), but the binary draws listen ports from the full 2 × NUM_CHANNELS-wide range — empirically, sessions opened at different times end up on ports spread across the entire reserved range rather than packed contiguously from the base. A deployment configured with NUM_CHANNELS=200 therefore reserves 400 TCP ports (e.g. 20000–20399), expects any of them to be bound at any time, and uses up to 200 of them simultaneously.

Why the MRCP range is sized at 2 × NUM_CHANNELS:

  • The binary reserves the doubled width and allocates anywhere within it, so firewall / ports: block / OS sysctl rules must permit the entire range.
  • Keeping the MRCP and RTP ranges the same width makes the configuration uniform and removes one source of subtle off-by-one mistakes.

Sizing either range as one port per channel (i.e. matching NUM_CHANNELS rather than doubling it) is an error: for the RTP range it silently caps real-world capacity at roughly half of NUM_CHANNELS; for the MRCP range it leaves the binary unable to bind ports it expects to have available (once it tries to allocate above MRCP_PORT_BASE + NUM_CHANNELS − 1, the exposed range runs out).


How NUM_CHANNELS affects port usage

MEDIA_SERVER__NUM_CHANNELS controls the size of the MRCP and RTP port ranges the API is permitted to draw from. It does not pre-bind every port at startup.

A correctly running idle container will have effectively zero sockets in the MRCP control range and the RTP media range. As calls arrive, the API allocates ports out of those ranges; as calls end, it releases them. You can observe this with ss -tn '( sport >= :20000 and sport <= :20200 )' (or the equivalent for UDP) on the host.

In normal operation:

  • concurrent_active_channels ≈ TCP MRCP ports in use (any port across the reserved MRCP range can be bound) ≈ ½ × UDP ports in use in the RTP range (because each call uses two UDP ports — one RTP, one RTCP).
  • The peak observed port count tracks the peak concurrency.
  • NUM_CHANNELS is the ceiling. Setting it to a larger value than you ever expect to use is harmless: unused ports are simply not bound.
  • NUM_CHANNELS is not a throttle below its configured value. The API will happily run at any concurrency up to the limit, subject to host resources.

Configuration consistency rules

The three places where port-related settings appear must all agree:

  1. MEDIA_SERVER__NUM_CHANNELS — the API's internal limit.
  2. MEDIA_SERVER__MRCP_PORT_BASE and MEDIA_SERVER__RTP_PORT_BASE — the starting port of each range. (See OPTIONAL.md.)
  3. The ports: block in docker-compose.yml — only relevant when not using network_mode: host (see next section).

Rules to keep these aligned:

  • The exposed MRCP TCP port range size must equal 2 × NUM_CHANNELS. If NUM_CHANNELS=200, expose 400 TCP ports starting at MRCP_PORT_BASE (for example 20000–20399). The binary draws listen ports from anywhere in this range — at peak concurrency NUM_CHANNELS of them are bound, but which specific ports varies as sessions start and end.
  • The exposed RTP UDP port range size must equal 2 × NUM_CHANNELS. If NUM_CHANNELS=200, expose 400 UDP ports starting at RTP_PORT_BASE (for example 25000–25399). The full range is actively used (RTP on even ports, RTCP on odd).
  • MRCP_PORT_BASE + 2 × NUM_CHANNELS and RTP_PORT_BASE + 2 × NUM_CHANNELS must both stay below 65536. A high port base combined with a large NUM_CHANNELS will overrun the IP port space and cause allocation to fail.
  • When network_mode: host is set, the ports: block must be commented out or removed. See below.

network_mode: host versus bridge networking

Docker offers two relevant network modes for this container:

network_mode: host (recommended)

With host networking, the container shares the host's network stack directly. There is no port translation layer and no per-port helper process. When the MRCP-API binary calls listen() on, say, port 5060, it binds to port 5060 on the host.

Consequences:

  • The ports: mapping in docker-compose.yml has no effect under host networking — there is no separate network namespace to map from. On some Docker Compose versions it is silently ignored; on others it produces an error such as ERROR: for cloud-media-server "host" network_mode is incompatible with port_bindings. Comment out or remove the ports: block when running with network_mode: host.
  • Host firewalls (iptables, ufw, cloud security groups) must allow inbound traffic to the relevant ports — there is no Docker port mapping to intermediate.
  • The host OS must have those ports free at the time the container starts.

Bridge networking (the default, if network_mode is omitted)

With bridge networking, the container has its own network namespace and Docker spawns a userland helper (docker-proxy) for every published port. For a deployment with NUM_CHANNELS=200 this is roughly 800 helper processes (400 for the MRCP control range, 400 for the RTP/RTCP pairs), plus a handful for SIP, SIPS and RTSP. Each helper consumes file descriptors and memory.

Consequences:

  • The ports: block is required in this mode, and the ranges in it must match the API's internal port ranges (including the doubled RTP range).
  • Memory usage of the deployment increases proportionally with the number of published ports. For large NUM_CHANNELS values this can be significant.
  • You can avoid the per-port helper processes by setting "userland-proxy": false in /etc/docker/daemon.json and restarting Docker.

In nearly all production deployments, network_mode: host is the simpler and lighter choice. The example docker-compose.yml in this repository uses it for that reason.


Per-call port usage in practice

For each active call:

  • One TCP socket is bound in the MRCP control range. The binary may allocate the listen port anywhere within MRCP_PORT_BASE..MRCP_PORT_BASE + 2 × NUM_CHANNELS − 1, so the entire reserved range must be available; at peak concurrency, NUM_CHANNELS of those ports are simultaneously bound.
  • Two UDP sockets are bound in the RTP media range — one even-numbered port for RTP and the adjacent odd port for RTCP, both inside RTP_PORT_BASE..RTP_PORT_BASE + 2 × NUM_CHANNELS − 1.
  • For plain SIP (UDP or TCP) there is no extra per-call signalling socket — INVITEs flow over the existing listener on port 5060.
  • For SIPS (SIP over TLS) there is one additional long-lived TLS TCP connection per call on port 5061. SIPS is connection-oriented, so each call keeps its own TLS socket open for the duration of the session.

Under sustained traffic:

  • The TCP MRCP port count tracks the active call count closely.
  • The UDP RTP+RTCP port count tracks twice the active call count, with brief overshoots from sockets lingering after a call ends.
  • The SIPS socket count (port 5061) tracks the active call count when TLS signalling is in use.
  • When all calls drain, all counts return to near zero.

TLS, SIPS, MRCPS and SRTP

The MRCP-API supports running the entire stack over encrypted transports:

LayerPlaintextEncrypted formNotes
SIP signallingsip: on 5060sips: on 5061 (TLS over TCP)Different port; signalling needs MEDIA_SERVER__SIPS_SSL_CERT_FILE and a corresponding key.
MRCPv2 controlTCPTCP/TLS (RFC 6230)Same TCP port range, just wrapped in TLS. Negotiated in the SDP via m=application N TCP/TLS/MRCPv2 and a server cert configured via MEDIA_SERVER__MRCP_TLS_CERT_FILE.
RTP media + RTCPUDP RTP/RTCPSRTP (RFC 3711)Same UDP port pair, the payload is encrypted and authenticated. Negotiated via SDP m=audio N RTP/SAVP … and a=crypto: lines.

What changes in the port picture when TLS is enabled

  • MRCP control: no port change. MRCPS uses the same TCP port range as plain MRCP; only the payload is wrapped in TLS.
  • RTP media: no port change. SRTP uses the same RTP/RTCP port pair as plain RTP; only the payload is encrypted.
  • SIPS adds one long-lived socket per call. Plain SIP signalling holds only a handful of listening sockets; SIPS, being connection-oriented TLS, keeps a fresh socket open on port 5061 for every active call. Customers sizing for SIPS should budget for one extra long-lived TCP socket per channel on top of MRCP and RTP/RTCP. File-descriptor limits inside the container need to comfortably accommodate this.

So under SIPS + MRCPS + SRTP, the per-call socket budget is:

  • 1 SIPS TCP socket (port 5061)
  • 1 MRCPS TCP socket (anywhere in MRCP_PORT_BASE..MRCP_PORT_BASE + 2 × NUM_CHANNELS − 1)
  • 2 SRTP UDP sockets (in RTP_PORT_BASE..RTP_PORT_BASE + 2 × NUM_CHANNELS − 1)

Total: 4 sockets per active call under encrypted transport, vs 3 per call under plaintext (no per-call SIP socket).

Relevant environment variables

Configure encryption via the following (see OPTIONAL.md for the full list). The variables split into two distinct concerns — what the MRCP-API presents to its SIP/MRCP clients (client-facing TLS), and how the MRCP-API connects to the upstream LumenVox API (upstream TLS):

Client-facing TLS (SIPS / MRCPS / SRTP) — what calls into the MRCP-API:

VariablePurpose
MEDIA_SERVER__SIPS_PORTSIPS listener port (default 5061).
MEDIA_SERVER__SIPS_SSL_CERT_FILEPEM file containing the server certificate (and key) presented for SIPS. Configuring this is what enables SIPS.
MEDIA_SERVER__SIPS_CIPHER_LISTOpenSSL cipher list for SIPS.
MEDIA_SERVER__MRCP_TLS_CERT_FILEPEM file containing the server cert presented for MRCPS. Configuring this is what enables MRCPS.
MEDIA_SERVER__MRCP_CIPHER_LISTOpenSSL cipher list for MRCPS.
MEDIA_SERVER__ALLOW_UNAUTHENTICATED_SRTPWhether SRTP without authentication is accepted on the media leg.
MEDIA_SERVER__ALLOW_UNENCRYPTED_SRTPWhether plain RTP is accepted when the call advertises RTP/SAVP or is a fallback path.

Upstream TLS (MRCP-API → LumenVox API) — outbound gRPC, unrelated to client-facing TLS:

VariablePurpose
MEDIA_SERVER__USE_TLSWhether the MRCP-API uses TLS for its outbound connection to the upstream LumenVox API. Does not affect what clients see when connecting to the MRCP-API.
MEDIA_SERVER__SSL_VERIFY_PEERWhether the MRCP-API verifies the upstream LumenVox API's certificate. Also does not affect client-facing TLS.

These two variables are a common point of confusion: both are scoped to the outbound leg of the MRCP-API only. Whether SIPS/MRCPS is offered to clients is controlled by the *_SSL_CERT_FILE / *_TLS_CERT_FILE variables in the upper table, not by USE_TLS.

How TLS / SRTP are negotiated in the SDP

When a SIP client offers an encrypted media session, the SDP changes in two ways relative to the plaintext form:

  • The m=application … line changes its profile from TCP/MRCPv2 to TCP/TLS/MRCPv2.
  • The m=audio … line changes its profile from RTP/AVP to RTP/SAVP, and an a=crypto: attribute carrying the SRTP master key + salt is added. A typical line looks like:
m=audio 30200 RTP/SAVP 0 101
a=rtpmap:0 pcmu/8000
a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:<base64-30-bytes>

The server's answer mirrors these decisions: if it accepts the offered suite it answers with RTP/SAVP and its own a=crypto:. If it rejects the offer (commonly because ALLOW_UNENCRYPTED_SRTP is not enabled and the suite isn't acceptable) it typically responds with 486 Busy Here containing a counter-offer indicating the parameters it would have accepted.

Performance considerations under TLS

Encryption adds modest cost. Empirically, on a 4-core Ubuntu VM with NUM_CHANNELS=400:

  • TLS signalling + MRCPS (over plain RTP) adds a per-call CPU cost noticeably above plaintext, and adds the SIPS socket per call described above. Memory growth is roughly half a megabyte per active call.
  • SRTP on top of TLS is, by comparison, nearly free in the data plane — AES-CM-128 + HMAC-SHA1 are inexpensive and add a few percent of CPU overhead at most. Memory per call grows only a fraction of a megabyte.

In practice, when sizing for an encrypted deployment, the binding constraints in order are: (1) host CPU under load, (2) upstream LumenVox API latency, and only then (3) any TLS-specific overhead.


Troubleshooting: "running out of ports"

If a deployment is failing to allocate ports, work through the following in order. The most common causes are at the top.

1. Configuration mismatch

  • Confirm that MEDIA_SERVER__NUM_CHANNELS and the size of the MRCP and RTP port ranges all agree (and, if bridge networking is in use, that the ports: block matches). Both ranges must be 2 × NUM_CHANNELS ports wide — the RTP range fills via RTP/RTCP pairs, while the MRCP range fills only in its lower half but still needs to be exposed end-to-end.
  • Confirm that MRCP_PORT_BASE + 2 × NUM_CHANNELS and RTP_PORT_BASE + 2 × NUM_CHANNELS are both less than 65536.
  • If network_mode: host is set and the ports: block is also present, comment out or remove the ports: block.

Commands:

grep -E 'NUM_CHANNELS|PORT_BASE' docker-compose.yml .env
docker inspect cloud-media-server \
    --format '{{range .Config.Env}}{{println .}}{{end}}' \
  | grep -E 'NUM_CHANNELS|PORT_BASE|SIPS|TLS|SRTP'
docker inspect cloud-media-server --format '{{.HostConfig.NetworkMode}}'

2. Docker proxy overhead (bridge mode only)

If network_mode is not host and the deployment exposes hundreds of ports, you may be hitting resource limits caused by the per-port docker-proxy processes rather than by the MRCP-API itself. Remember that the RTP range alone produces 2 × NUM_CHANNELS proxy processes when bridged, so this scales fast.

  • Switch to network_mode: host, or
  • Set "userland-proxy": false in /etc/docker/daemon.json and restart Docker.

3. Operating-system limits

The remaining causes live below the application. On the host:

  • Ephemeral port range. Outbound TCP connections (gRPC to the upstream LumenVox API, etc.) draw from the kernel's ephemeral range, which defaults to 32768 60999 on Linux. Under high outbound connection rates this can be exhausted independently of the MRCP/RTP ranges. Widen with:
sysctl -w net.ipv4.ip_local_port_range="10000 65535"
  • TIME_WAIT accumulation. Closed TCP sockets sit in TIME_WAIT for about 60 seconds and continue to occupy an ephemeral port. Under high call rates these accumulate — especially under SIPS, where every call closes its own TLS connection. Check with ss -tan state time-wait | wc -l and consider:
sysctl -w net.ipv4.tcp_tw_reuse=1
  • File descriptor limit (ulimit -n). Every socket is a file descriptor. Default soft limits are often 1024, which is too low for a busy MRCP-API container — especially under SIPS, where the per-call socket budget is 4 rather than 3. The container in this repository normally runs with ulimit -n = 1048576 inside; verify with:
docker exec cloud-media-server sh -c 'ulimit -Sn'

If steps 1 and 2 are clean, the issue is almost certainly at this OS layer rather than in the MRCP-API itself.


What NUM_CHANNELS does not do

To avoid common misunderstandings, it is worth being explicit about a few things NUM_CHANNELS does not control:

  • It does not pre-bind any ports.
  • It does not pre-allocate per-channel memory or threads.
  • It does not set a target concurrency — calls are admitted on demand up to the limit.
  • It does not, by itself, determine real-world throughput. The practical ceiling of a deployment is a combination of host CPU, network latency to the upstream LumenVox API, and the ASR/TTS workload, and is typically lower than NUM_CHANNELS on any given host. Sizing the host appropriately matters more than raising NUM_CHANNELS further.

Was this article helpful?