Every multiplayer game eventually hits the same wall: players want to talk to each other before, during, and after a match, but building voice infrastructure from scratch means signaling servers, STUN/TURN relays, codec licensing, and a team dedicated to keeping calls connected across flaky mobile networks. Most studios never get past the prototype stage. Azure Communication Services (ACS) removes that burden by handing you a managed calling and chat backend that bills per minute and per message, with no servers to patch and no media relays to run yourself.
This tutorial walks through building a working pregame lobby voice chat system for a multiplayer game using Azure Communication Services: provisioning the resource, issuing access tokens safely, wiring up the Calling SDK in a web lobby, adding group voice rooms, and handling the token refresh and reconnection edge cases that trip up most first attempts. By the end you will have a deployable lobby app where players join a queue, talk over voice while waiting, and get dropped into a muted state the instant the match starts.
Note upfront: Azure Communication Services does not ship an official Unity or Unreal Engine SDK as of October 2026. It is a web, iOS, Android, and Windows SDK. That makes it the wrong tool if you want voice chat rendered inside a 3D game engine viewport. It is the right tool if you are building a browser-based or mobile companion lobby, a tournament bracket app, a spectator chat overlay, or a pregame/postgame hub that sits outside the engine itself, which is exactly the pattern this tutorial builds.
Don't miss new tech stories on Google
Add Tech Insider once in the Google app and our stories appear in your news suggestions.
What Is Azure Communication Services and Why Use It for Game Lobbies
Azure Communication Services is Microsoft’s fully managed communication platform, offering voice calling, video calling, chat, and SMS/telephony through REST APIs and client SDKs, built on the same infrastructure that runs Microsoft Teams calling. Unlike Vivox or Agora, which were purpose-built for games, ACS was built as a general communications platform-as-a-service, competing directly with Twilio and MessageBird. That general-purpose design is exactly why it works well for the lobby-and-companion-app layer of a game: you get enterprise-grade call reliability, global VoIP reach, and Azure-native identity integration without adopting a games-only vendor.
For a game studio already running backend services on Azure, Kubernetes, or a mixed-cloud stack, ACS slots in as just another Azure resource, provisioned through the same Azure CLI, Bicep, or Terraform pipelines used for everything else. That matters for studios practicing infrastructure-as-code discipline across their cloud estate: voice chat becomes one more resource definition rather than a bolt-on vendor integration with its own separate console and billing portal.
The core use case this tutorial targets is the pregame lobby: a waiting room where matched or queued players can talk while the match is being assembled, independent of whatever engine (Unity, Unreal, Godot, or a custom engine) renders the actual gameplay. Studios running mobile live-service games, web-based card and board games, or companion apps for console titles get the most direct value here, since those surfaces are already web or mobile-native.
How a Lobby Voice Call Actually Flows Through Azure
Understanding the call path makes the later debugging steps far less mysterious. When a player’s browser calls callAgent.join, the Calling SDK first negotiates with ACS’s signaling layer to register the client as present in that groupId room. From there, media packets (the actual audio) do not necessarily travel through a single Microsoft datacenter; they route through Microsoft’s global network of media relays, which pick the shortest viable path between participants using the same ICE/STUN/TURN negotiation pattern any WebRTC system relies on, except Microsoft operates and scales the relay fleet for you instead of you running coturn or a similar TURN server yourself.
This is the actual value proposition of a managed service like ACS over a raw WebRTC implementation: you are not writing NAT traversal logic, you are not provisioning TURN relay capacity ahead of a weekend tournament spike, and you are not debugging why one player’s corporate firewall blocks UDP while another’s does not. The tradeoff is that you cannot see or tune the relay topology yourself, which matters if you later need extremely specific latency guarantees that a general-purpose platform cannot promise.
Identity and token issuance, by contrast, is entirely under your control, which is exactly why Step 2 of this tutorial spends so much time on it. The CommunicationIdentityClient calls are REST requests against your ACS resource’s management plane, not the real-time media plane, so they behave like any other backend API call: synchronous, easy to log, and easy to rate-limit if you need to.
Prerequisites and Versions
Confirm each of the following before starting. Version drift between the Azure CLI, Node.js, and the ACS SDK is the single most common cause of broken quickstarts reported in Microsoft’s own sample repositories.
- An active Azure subscription with permission to create resources (a free Azure account works for this tutorial; ACS itself has no free tier)
- Azure CLI version 2.65 or later (
az --versionto check) - Node.js 20 LTS or later and npm 10+ for the web lobby backend and frontend
@azure/communication-commonversion 2.3.1 or later@azure/communication-callingversion 1.33.1 or later (the Calling SDK)@azure/communication-identityversion 1.3.1 or later (server-side token issuance)- A modern browser for testing: Chrome 110+, Edge 110+, Safari 16+, or Firefox 115+ (ACS Calling SDK requires WebRTC support)
- A code editor and basic familiarity with Express.js or a similar Node.js server framework
- A credit card on the Azure account, since ACS requires a pay-as-you-go subscription and has no sandbox-only tier
Budget roughly 90 minutes for the full walkthrough, including resource provisioning, SDK wiring, and testing a real two-participant call.
Step 1: Provision an Azure Communication Services Resource
Every ACS project starts with a Communication Services resource, which is the top-level container that issues identities, connection strings, and usage billing. Create one through the Azure CLI rather than the portal so the step is reproducible in your CI pipeline later.
az login
az group create --name game-lobby-rg --location eastus
az communication create \
--name game-lobby-acs \
--resource-group game-lobby-rg \
--location global \
--data-location UnitedStates
The --data-location flag controls where ACS stores chat and identity metadata at rest, which matters for studios with players in the EU who need data residency guarantees. The actual VoIP media path is global and dynamically routed regardless of this setting; it only affects where your account’s metadata lives.
Once the resource finishes provisioning, grab the connection string, which you will need on the server side to issue access tokens. Never expose this string to a client app.
az communication list-key \
--name game-lobby-acs \
--resource-group game-lobby-rg
Store the returned primaryConnectionString in an environment variable or your secrets manager (Azure Key Vault, if you are already using it elsewhere in your stack). Hardcoding it into a repository is the single most common security mistake in ACS integrations and is called out explicitly in Microsoft’s own security guidance for the service.
Step 2: Build the Token Issuance Server
Azure Communication Services authenticates every client connection with a short-lived access token tied to an ACS identity, not your game’s player account system directly. Your backend is responsible for mapping a logged-in player to an ACS identity and minting that token on demand. This is the piece almost every tutorial glosses over, and it is the piece that determines whether your lobby chat is secure.
mkdir lobby-token-server && cd lobby-token-server
npm init -y
npm install express @azure/communication-identity dotenv cors
// server.js
require('dotenv').config();
const express = require('express');
const cors = require('cors');
const { CommunicationIdentityClient } = require('@azure/communication-identity');
const app = express();
app.use(cors());
app.use(express.json());
const identityClient = new CommunicationIdentityClient(
process.env.ACS_CONNECTION_STRING
);
// Map a game account to a stable ACS identity, stored in your own DB in production
const playerIdentityStore = new Map();
app.post('/api/lobby/token', async (req, res) => {
const { playerId } = req.body;
if (!playerId) {
return res.status(400).json({ error: 'playerId is required' });
}
try {
let acsUserId = playerIdentityStore.get(playerId);
if (!acsUserId) {
const identityResponse = await identityClient.createUser();
acsUserId = identityResponse.communicationUserId;
playerIdentityStore.set(playerId, acsUserId);
}
const tokenResponse = await identityClient.getToken(
{ communicationUserId: acsUserId },
['voip', 'chat']
);
res.json({
token: tokenResponse.token,
expiresOn: tokenResponse.expiresOn,
acsUserId,
});
} catch (err) {
console.error('Token issuance failed:', err.message);
res.status(500).json({ error: 'token issuance failed' });
}
});
app.listen(3001, () => console.log('Lobby token server on port 3001'));
Keep a persistent mapping between your game’s player ID and the ACS communicationUserId in a real database, not the in-memory Map shown above, which only exists to keep the example readable. Losing that mapping means a returning player gets issued a brand-new ACS identity every session, which breaks any feature that depends on recognizing the same player across calls, such as a mute list or a block list.
Step 3: Set Up the Web Lobby Frontend
With token issuance working, build the lobby page that players land on after queuing for a match. This is a plain web app, deployable to any static host or Azure Static Web Apps.
mkdir lobby-frontend && cd lobby-frontend
npm init -y
npm install @azure/communication-common @azure/communication-calling vite
// lobby.js
import { CallClient } from '@azure/communication-calling';
import { AzureCommunicationTokenCredential } from '@azure/communication-common';
let callAgent;
let call;
async function initLobby(playerId) {
const response = await fetch('http://localhost:3001/api/lobby/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ playerId }),
});
const { token, acsUserId } = await response.json();
const tokenCredential = new AzureCommunicationTokenCredential(token);
const callClient = new CallClient();
callAgent = await callClient.createCallAgent(tokenCredential, {
displayName: playerId,
});
console.log('Lobby voice ready for', acsUserId);
return callAgent;
}
async function joinLobbyRoom(groupId) {
call = callAgent.join(
{ groupId },
{ audioOptions: { muted: false } }
);
call.on('stateChanged', () => {
console.log('Call state:', call.state);
document.getElementById('call-state').textContent = call.state;
});
call.remoteParticipants.forEach(trackRemoteParticipant);
call.on('remoteParticipantsUpdated', (e) => {
e.added.forEach(trackRemoteParticipant);
});
}
function trackRemoteParticipant(participant) {
console.log('Participant joined lobby voice:', participant.identifier);
}
function leaveLobbyRoom() {
if (call) call.hangUp();
}
document.getElementById('join-btn').addEventListener('click', async () => {
const playerId = document.getElementById('player-id').value;
const groupId = document.getElementById('group-id').value;
await initLobby(playerId);
await joinLobbyRoom(groupId);
});
document.getElementById('leave-btn').addEventListener('click', leaveLobbyRoom);
The groupId is a GUID shared by every player placed in the same matchmaking lobby. Your matchmaking service should generate this GUID at the moment it forms a lobby group and hand it to every client in that group, so they all call join on the same voice room.
Step 4: Add Mute, Push-to-Talk, and Participant Lists
A bare voice connection is not a usable lobby chat. Players expect to see who else is talking, mute themselves, and in competitive titles, use push-to-talk rather than open mic to avoid flooding the lobby with background noise. These controls sit on top of the call object from Step 3.
async function toggleMute() {
if (call.isMuted) {
await call.unmute();
} else {
await call.mute();
}
document.getElementById('mute-btn').textContent = call.isMuted ? 'Unmute' : 'Mute';
}
// Push-to-talk: mute by default, only unmute while a key is held
let pushToTalkActive = false;
document.addEventListener('keydown', async (e) => {
if (e.code === 'KeyV' && !pushToTalkActive) {
pushToTalkActive = true;
await call.unmute();
}
});
document.addEventListener('keyup', async (e) => {
if (e.code === 'KeyV') {
pushToTalkActive = false;
await call.mute();
}
});
function renderParticipantList() {
const list = document.getElementById('participant-list');
list.innerHTML = '';
call.remoteParticipants.forEach((p) => {
const item = document.createElement('li');
const speaking = p.isSpeaking ? ' (speaking)' : '';
item.textContent = `${p.displayName || 'Player'}${speaking}`;
list.appendChild(item);
});
}
call.on('remoteParticipantsUpdated', renderParticipantList);
setInterval(renderParticipantList, 1000);
Start every lobby call muted by default rather than open mic. Players joining a queue of strangers expect to opt into talking, and defaulting to open mic is the fastest way to generate player complaints about hearing someone else’s living room before the match even starts.
Step 5: Handle Token Refresh Before It Drops Calls
ACS access tokens expire, by default within 24 hours but configurable down to as little as 60 minutes when you call getToken, per the official Calling SDK quickstart. A lobby session that outlasts the token’s lifetime will silently disconnect the player mid-call if you do not refresh it proactively. This is one of the most frequently reported issues in ACS community forums, and it is entirely avoidable.
function setupTokenRefresh(playerId) {
return new AzureCommunicationTokenCredential({
tokenRefresher: async () => {
const response = await fetch('http://localhost:3001/api/lobby/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ playerId }),
});
const { token } = await response.json();
return token;
},
refreshProactively: true,
});
}
Pass this refreshing credential into createCallAgent instead of a static token string, and the SDK will call your refresher in the background before expiry, keeping long matchmaking queues or marathon tournament lobbies connected without a reconnect.
Step 6: Mute the Lobby and Hand Off When the Match Starts
The lobby voice room should not simply keep running once a match begins, since players are usually switching to in-game voice (if the engine has one) or a different channel entirely. Wire the lobby to listen for a match-start signal from your matchmaking backend and tear down cleanly.
function onMatchStartSignal(matchDetails) {
// Called when your matchmaking service pushes a match-found event,
// e.g. over a WebSocket or SignalR connection you already run for matchmaking
document.getElementById('status').textContent = 'Match found, leaving lobby voice...';
leaveLobbyRoom();
launchGameClient(matchDetails);
}
Hanging up explicitly rather than letting the browser tab close or navigate away matters because ACS bills by connected participant-minute. An orphaned call that never receives a hangUp keeps billing until the SDK’s own timeout kicks in, typically after several minutes of inactivity, which adds up at scale across thousands of concurrent lobbies.
Step 7: Add Text Chat Alongside Voice
Not every player wants to use voice, and some lobbies (ranked queues with strangers, in particular) are more comfortable with text. ACS Chat runs on a separate SDK from Calling but shares the same identity and token model, so adding it is additive rather than a second integration.
npm install @azure/communication-chat
import { ChatClient } from '@azure/communication-chat';
async function initLobbyChat(token, endpoint, threadId) {
const chatClient = new ChatClient(endpoint, new AzureCommunicationTokenCredential(token));
const chatThreadClient = chatClient.getChatThreadClient(threadId);
chatClient.startRealtimeNotifications();
chatClient.on('chatMessageReceived', (event) => {
renderChatMessage(event.sender, event.message);
});
return chatThreadClient;
}
async function sendLobbyMessage(chatThreadClient, text) {
await chatThreadClient.sendMessage({ content: text });
}
Create the chat thread server-side at the same moment your matchmaking service generates the lobby’s voice groupId, and pass both IDs to clients together. Treating voice and chat as two halves of the same lobby session, created and destroyed in lockstep, avoids a class of bugs where one channel outlives the other.
Step 8: Deploy the Token Server Securely
The Node.js token server from Step 2 cannot stay on localhost. Deploy it behind authentication so only your actual game client, holding a valid session for a logged-in player, can request an ACS token. Azure App Service or Azure Container Apps both work well here since they sit in the same region and billing account as your ACS resource.
az webapp up \
--name game-lobby-token-server \
--resource-group game-lobby-rg \
--runtime "NODE:20-lts" \
--sku B1
Add a middleware check that validates the caller’s game session token (from your existing player auth system) before it ever calls identityClient.getToken. Without this check, anyone who finds your token endpoint URL can mint unlimited ACS identities and run up your bill, since nothing about the endpoint itself is tied to your game’s player accounts unless you enforce it.
Step 9: Test a Real Two-Participant Lobby Call
Open the lobby frontend in two separate browser profiles (not two tabs in the same profile, since some browsers share microphone permission state oddly across tabs), each with a different playerId and the same groupId. Confirm:
- Both clients transition to a
connectedcall state - Muting one participant is reflected in the other’s participant list within roughly one second
- Push-to-talk, if implemented, actually gates the mic rather than just the UI label
- Closing one browser tab correctly fires a disconnect event on the other client
If audio connects but is one-directional, it is almost always a browser microphone permission prompt that was silently denied on one side. Check navigator.mediaDevices.getUserMedia permission state directly in devtools before assuming the SDK integration is broken.
Step 10: Monitor Usage and Set Cost Alerts
Because ACS bills per participant-minute with no included free tier, set a budget alert before this ever reaches production traffic. Azure Monitor and Cost Management cover ACS the same as any other Azure resource.
az consumption budget create \
--budget-name acs-lobby-budget \
--amount 200 \
--resource-group game-lobby-rg \
--time-grain Monthly \
--start-date 2026-10-01 \
--end-date 2027-10-01
Pair the budget alert with Azure Monitor diagnostic logs on the Communication Services resource to track call volume by hour, which is the fastest way to spot a bug that leaves calls connected after players have already left the lobby.
Step 11: Scale the Token Server Horizontally
A single Node.js instance issuing tokens becomes a bottleneck once concurrent lobby formation spikes, such as right after a patch drops or during a weekend tournament. Since token issuance is stateless aside from the identity mapping lookup, scale the token server horizontally behind a load balancer and move the identity mapping into a shared store like Azure Cosmos DB or Azure Cache for Redis rather than in-process memory.
az containerapp create \
--name lobby-token-server \
--resource-group game-lobby-rg \
--image yourregistry.azurecr.io/lobby-token-server:latest \
--min-replicas 2 \
--max-replicas 10 \
--scale-rule-name http-scale \
--scale-rule-http-concurrency 50
Azure Container Apps’ built-in autoscaling on concurrent HTTP requests handles the typical spiky pattern of lobby formation well, since token requests cluster tightly around matchmaking events rather than arriving at a steady rate.
Step 12: Handle Regional Latency for Global Player Bases
ACS VoIP calling and chat run on Microsoft’s global network and are available worldwide, but telephony-specific features are limited to a smaller set of countries including the United States, Canada, the United Kingdom, Ireland, Denmark, Italy, and Sweden. For a pure in-app voice lobby (no PSTN dial-in), this distinction rarely matters, but if a later feature adds phone-number calling into the lobby, confirm telephony availability for your target markets before committing to the feature.
For data residency, consult Microsoft’s data residency and privacy documentation and set the resource’s data location to the geography closest to your primary player base at creation time, since this setting cannot be changed after the resource exists. A studio with a mixed US and EU player base typically needs two separate ACS resources, one per data location, routed to by a thin backend layer that picks the right resource based on the player’s region.
Step 13: Add Reconnection Handling for Flaky Mobile Networks
Mobile players switching between Wi-Fi and cellular mid-queue is one of the most common real-world disruptions to a lobby call. The Calling SDK exposes connection state transitions that let you distinguish a graceful hangup from a network drop and attempt an automatic rejoin.
call.on('stateChanged', async () => {
if (call.state === 'Disconnected' && call.callEndReason?.code !== 0) {
console.warn('Lobby call dropped unexpectedly, attempting rejoin');
await joinLobbyRoom(currentGroupId);
}
});
A callEndReason.code of 0 indicates a normal, user-initiated hangup; nonzero codes indicate a network or service-side disconnect worth retrying automatically. Cap retries (three attempts with backoff is a reasonable default) so a genuinely offline player does not loop forever trying to reconnect.
Common Pitfalls When Building ACS Voice Chat for Games
These are the mistakes that show up repeatedly in community forums and Microsoft’s own support channels when teams integrate ACS into a game-adjacent product. Most of them are not exotic; they are the same handful of oversights that show up whenever a team treats a managed communications platform like a simple drop-in library rather than a billed, stateful service with its own lifecycle and security surface.
- Expecting a Unity or Unreal SDK. ACS officially supports Web, iOS, Android, and Windows. There is no first-party Unity or Unreal plugin as of October 2026; community plugins exist but are unofficial and unmaintained by Microsoft.
- Exposing the connection string to the client. The connection string grants full control over your ACS resource, including the ability to mint unlimited identities. It belongs only on the token server, never in frontend code or a mobile app binary.
- Letting tokens expire mid-session. Without a
tokenRefresher, a long-running lobby will silently disconnect once the token’s lifetime elapses, which defaults to 24 hours but is often set shorter. - Defaulting new joiners to open mic. Unmuted-by-default lobbies generate a disproportionate number of player complaints compared to the minor friction of asking players to unmute themselves.
- Not cleaning up orphaned calls. A tab closed without an explicit
hangUpkeeps billing for a stretch before the SDK’s own timeout fires, which is wasted spend at any real scale. - Ignoring data residency until after launch. The data location setting is fixed at resource creation. Studios that later need EU residency for GDPR reasons must provision an entirely new resource and migrate, rather than reconfigure the existing one.
- Treating ACS as a drop-in Vivox or Agora replacement. ACS is a general communications platform, not a games-specific SDK. It lacks built-in positional/proximity audio, which Vivox offers natively; building spatial audio on top of ACS requires custom work with the Web Audio API.
- Skipping load testing on the token server. A sudden spike in concurrent matchmaking (common right after a content patch) can overwhelm a single-instance token server before anyone notices a problem in the game itself.
Troubleshooting Guide
Work through this list in order when a lobby call fails to connect or behaves unexpectedly.
- Call never reaches “connected” state: confirm both clients are joining the same
groupIdexactly; a mismatched GUID (including casing differences) silently creates two separate rooms instead of erroring. - “Access token is invalid” error on join: the token server’s connection string environment variable is likely missing or stale after a resource key rotation; re-fetch it with
az communication list-key. - Audio connects but is silent one direction: check browser microphone permissions directly, since a silently denied prompt does not always surface as an SDK-level error.
- Call drops after exactly 24 hours of idle lobby time: this is token expiry; implement the
tokenRefresherpattern from Step 5. - “CORS error” on the token request: the Express
cors()middleware must explicitly allow your frontend’s deployed origin, not just localhost, once you move past local development. - Unexpectedly high monthly bill: check Azure Cost Management for orphaned calls left connected past their expected lobby duration; add explicit
hangUpcalls on page unload events. - Participants list never updates: confirm the
remoteParticipantsUpdatedlistener is attached beforejoinresolves, not after; a race here is a common source of a participant list that is one join behind reality. - Calls work on desktop but fail on mobile Safari: confirm you are on a current iOS version, since older WebKit builds have known WebRTC compatibility gaps with the Calling SDK documented in Microsoft’s known-issues list.
- Push-to-talk key doesn’t release the mic: a dropped
keyupevent (common when a browser tab loses focus mid-keypress) leaves the mic open; add ablurevent listener that forces a mute as a safety net.
Advanced Tips for Production Lobby Systems
Once the basic lobby works, these refinements separate a prototype from something that survives real player load.
Pre-warm the CallAgent the moment a player enters matchmaking queue rather than waiting until a match is found. Creating the call agent and acquiring microphone permission ahead of time removes several seconds of latency right at the moment players are most impatient, immediately after a match is found.
Layer a simple volume-based speaking indicator using the isSpeakingChanged event rather than building your own audio level detection; the SDK already computes this server-side and exposes it as a boolean, which is both simpler and more battery-efficient on mobile than running local audio analysis.
For competitive titles with reporting and moderation requirements, enable ACS’s built-in call recording and retrieve recordings through the Server Calling SDK rather than building a separate recording pipeline. This gives moderation teams an audio trail for harassment reports without a second integration.
Separate your matchmaking-formed lobbies from persistent social spaces like a guild or clan voice channel. It is tempting to reuse the same groupId pattern for both, but a matchmaking lobby has a short, predictable lifetime that maps cleanly to the billing model described later in this tutorial, while a persistent clan channel behaves more like an always-on room that needs its own lifecycle management, moderation tooling, and idle-timeout policy so it does not quietly accumulate cost while nobody is actually talking.
Log call quality metrics (jitter, packet loss, round-trip time) that the Calling SDK already surfaces through its diagnostics API, and ship them to whatever telemetry pipeline your studio already uses for crash and performance reporting. Lobby voice problems that look like an ACS outage are, in practice, usually a regional network issue affecting a handful of players, and having that data on hand turns a support ticket into a five-minute diagnosis instead of a day-long investigation.
If your lobby needs to support more than roughly 20 concurrent speakers (large tournament lobbies, for instance), check Microsoft’s service limits documentation and structure it as a large ACS group call rather than a standard group call, since the underlying media architecture differs and standard group calls are tuned for smaller conversational groups.
Complete Working Project Structure
The finished lobby voice system from this tutorial is organized as two deployable services plus shared configuration:
game-lobby-voice/
├── lobby-token-server/
│ ├── server.js
│ ├── package.json
│ └── .env # ACS_CONNECTION_STRING, never committed
├── lobby-frontend/
│ ├── lobby.js
│ ├── index.html
│ ├── package.json
│ └── vite.config.js
├── infra/
│ ├── main.bicep # ACS resource + App Service/Container App
│ └── budget-alert.bicep
└── README.md
Deploy lobby-token-server to Azure Container Apps or App Service, deploy lobby-frontend to Azure Static Web Apps or any static host, and wire your existing matchmaking service to call the token server’s /api/lobby/token endpoint and hand the resulting groupId to both the lobby frontend and your game client via whatever real-time channel (WebSocket, SignalR, or your engine’s own networking layer) you already use for matchmaking callbacks.
Azure Communication Services vs Alternatives for Game Voice Chat
ACS is not the only option for adding voice to a multiplayer game, and it is not always the right one. The table below compares it against the services most commonly considered for the same job.
| Service | Unity/Unreal SDK | Built for games | Pricing model | Best fit |
|---|---|---|---|---|
| Azure Communication Services | No (community plugins only) | No | Per participant-minute, no free tier | Web/mobile lobbies, companion apps, tournament platforms |
| Vivox (Unity Gaming Services) | Yes, native | Yes | Tiered by monthly active users | In-engine 3D positional voice chat |
| Agora | Yes, official SDKs | Partially | Per minute, free tier available | Mobile games needing low-latency voice at scale |
| Twilio Programmable Voice | No | No | Per minute, pay-as-you-go | General app voice/SMS outside the game engine |
| Discord Voice SDK | No native engine SDK | Partially | Free for approved titles | Games willing to depend on the Discord app/overlay |
The practical takeaway: choose Vivox or Agora if voice needs to live inside the Unity or Unreal viewport with positional audio tied to in-game character position. Choose ACS if the voice experience lives in a web or mobile surface adjacent to the game, such as the lobby this tutorial builds, a tournament organizer dashboard, or a companion app, where Azure-native integration with the rest of your backend outweighs the lack of an engine plugin.
Teams already deep in the Azure ecosystem, running their matchmaking service on Azure Functions, their player database on Cosmos DB, or their backend on AKS, get an additional, less obvious benefit from ACS: a single Azure AD identity and billing boundary across the whole stack. That reduces the number of vendor relationships, separate API keys, and third-party status pages a small backend team has to track during an incident, which is a real operational cost even though it rarely shows up in a feature comparison table.
Azure Communication Services Pricing for Lobby Voice Chat
ACS pricing is consumption-based with no monthly minimums and no free tier, so costs scale directly with lobby usage. The table below summarizes the core rates relevant to a voice-and-chat lobby, drawn from Microsoft’s published pricing documentation.
| Component | Rate | Notes |
|---|---|---|
| VoIP group calling | $0.004 per participant per minute | Applies to both voice and video; audio-only does not cost less |
| Screen sharing | $0.004 per participant per minute | Billed the same as calling while active |
| Chat messages | Billed per message sent/read, consult current pricing page | Negligible cost relative to calling for most lobby use cases |
| Identity creation | No charge | Creating ACS user identities itself is free; usage is what’s billed |
| Recording (if enabled) | Separate per-minute rate plus storage | Only relevant if you enable call recording for moderation |
A rough estimate: a lobby that holds four players for an average of two minutes before a match starts costs approximately $0.032 per lobby session (4 participants × 2 minutes × $0.004). At 50,000 matches per day, that is roughly $1,600 per day in lobby voice costs alone, which is why Step 10’s budget alert is not optional for anything beyond a small-scale launch.
Frequently Asked Questions
Does Azure Communication Services work inside Unity or Unreal Engine?
Not with an official SDK. Microsoft publishes Web, iOS, Android, and Windows SDKs for ACS, but there is no first-party Unity or Unreal Engine package as of October 2026. Community-maintained Unity wrappers exist on GitHub, but they are unofficial and not supported by Microsoft. If voice needs to render inside the game engine itself with positional audio, Vivox or Agora are the more direct fit.
Is there a free tier for Azure Communication Services?
No. ACS requires a pay-as-you-go Azure subscription and bills from the first minute of use, with no included free allotment for calling or chat. Budget for this from day one rather than assuming an initial grace period.
How much latency should I expect for voice calls?
ACS runs on Microsoft’s global network and is designed for real-time communication, but Microsoft does not publish a single universal latency figure since actual performance depends on each player’s network path, device, and distance to the nearest Microsoft point of presence. Test from your actual target regions rather than relying on a published number.
Can I use Azure Communication Services for in-game PSTN phone calls?
Yes, but telephony (calling real phone numbers) is only available in a subset of countries, including the United States, Canada, the United Kingdom, Ireland, Denmark, Italy, and Sweden. VoIP calling and chat between app users, which is what this tutorial’s lobby voice system uses, are available globally and are not limited to those countries.
How do I prevent players from racking up unexpected ACS costs?
Set an Azure budget alert as shown in Step 10, enforce an explicit hangUp on page unload and match-start events so calls never run longer than intended, and monitor Azure Monitor diagnostic logs for calls that stay connected past your expected lobby duration, which usually indicates a client-side bug rather than legitimate usage.
What happens if a player’s access token expires mid-call?
Without a tokenRefresher configured, the call disconnects once the token’s validity window (by default up to 24 hours, but configurable shorter) elapses. Implementing the proactive refresh pattern from Step 5 avoids this entirely for any lobby session reasonably expected to run for an extended period.
Should I build spatial or proximity voice chat with ACS?
ACS has no built-in positional audio. You would need to compute distance-based volume attenuation yourself using the Web Audio API and feed that into how you mix remote participant streams, which is meaningfully more engineering work than Vivox’s native proximity voice feature. For a pregame lobby, where everyone is in the same virtual room rather than walking around a 3D space, this limitation does not matter.
Can the same ACS resource serve both the game lobby and a separate customer support chat feature?
Technically yes, since ACS resources are general-purpose, but it is better practice to provision a separate resource per use case so that usage, billing, and access tokens stay cleanly isolated. Mixing player-facing lobby traffic with internal support tooling on one resource makes cost attribution and access control unnecessarily messy.





