Skip to main content

Audio-only pricing

Accounts are automatically billed at the lower audio-only rate when no video tracks are present in a call. See our pricing page for details.
If even one participant has an active video track at any point, the call will be billed as a video call.

Set up an audio-only room

The best way to enforce an audio-only experience is to configure room-level permissions to allow audio only. This way, even if a participant tries to turn on their camera, it won’t work.
curl --request POST \
     --url https://api.daily.co/v1/rooms \
     --header 'Authorization: Bearer DAILY_API_KEY' \
     --header 'Content-Type: application/json' \
     --data '{"properties": {"permissions": {"canSend": ["audio"]}}}'
Note that screen sharing is also blocked by this configuration, since screen captures are treated as video tracks.

Best practices for audio quality

These recommendations apply to working with audio in general, not just audio-only applications.

Decouple <audio> elements from visual components

When <audio> tags are tied to visual elements (like an avatar), audio stops playing when the element scrolls out of view. Render <audio> elements separately from visual components:
return (
  <>
    <Container>
      <Header>Speakers</Header>
      {speakerTiles}
      <Header>Listeners</Header>
      {listenerTiles}
    </Container>
    <Audio participants={participants} />
  </>
);

Offload expensive processing to an AudioWorklet

An AudioWorklet executes custom audio processing in a separate thread for low-latency processing. Daily Prebuilt uses this approach to detect microphone audio.

Managing larger calls

For calls with more than ~5 participants, you need to actively manage audio to keep the experience usable. There are two complementary approaches: receive-side (limit which tracks each participant subscribes to) and send-side (limit who can unmute and speak).

Receive-side: subscribe to active speakers only

By default, Daily subscribes every participant to every other participant’s tracks. In a large audio-only call with many unmuted participants, this creates a cacophony of background noise and wastes bandwidth. Disable automatic subscriptions and manage them manually:
const call = Daily.createCallObject({
  subscribeToTracksAutomatically: false,
});
Then subscribe only to the most recent active speakers using the active-speaker-change event:
const MAX_SPEAKERS = 5;
const activeSpeakers = [];

call.on('active-speaker-change', (event) => {
  const id = event.activeSpeaker?.peerId;
  if (activeSpeakers.includes(id)) return;

  call.updateParticipant(id, { setSubscribedTracks: { audio: true } });
  activeSpeakers.unshift(id);

  if (activeSpeakers.length > MAX_SPEAKERS) {
    const removed = activeSpeakers.pop();
    call.updateParticipant(removed, { setSubscribedTracks: { audio: false } });
  }
});
For more details, see the track subscriptions guide. If you’re using Daily React, this behavior is built into the DailyAudio component:
import { DailyAudio } from '@daily-co/daily-react';

function CallComponent() {
  return <DailyAudio maxSpeakers={5} />;
}

Send-side: control who can speak

For moderated experiences — webinars, Q&As, stage-based calls — you’ll want to limit how many participants can unmute at once. There are two approaches depending on whether participants should start muted or be promoted dynamically.

Start all participants as listeners

Set canSend to False at the room level so participants join as listeners by default, then grant audio permission individually:
# Room where all participants join as listeners
curl --request POST \
     --url https://api.daily.co/v1/rooms \
     --header 'Authorization: Bearer DAILY_API_KEY' \
     --header 'Content-Type: application/json' \
     --data '{"properties": {"permissions": {"canSend": false}}}'
An owner can then promote a participant to speaker by calling updateParticipant() with updated permissions:
// Grant audio send permission to a specific participant
call.updateParticipant(participantSessionId, {
  updatePermissions: {
    canSend: new Set(['audio']),
  },
});

// Revoke it (mute them server-side)
call.updateParticipant(participantSessionId, {
  updatePermissions: {
    canSend: False,
  },
});
updateParticipant() with updatePermissions requires the caller to have canAdmin: ['participants'] permission (i.e. they must be an owner or have been granted admin rights).

Hand raising

Rather than polling or using app messages, use setUserData() to broadcast raise/lower hand state. Each participant controls their own userData, and changes propagate to all participants via participant-updated events:
// Participant raises their hand
call.setUserData({ handRaised: true });

// Participant lowers their hand
call.setUserData({ handRaised: false });
Listen for participant-updated to update your UI when any participant’s hand state changes:
call.on('participant-updated', (event) => {
  const { participant } = event;
  if (participant.userData?.handRaised) {
    // show raised hand indicator for this participant
  }
});
An owner can then call updateParticipant() to grant the raised-hand participant canSend: ['audio'].

Analyze performance issues

Audio-only calls require relatively low bandwidth and CPU compared to video calls. If you see CPU or stale WebSocket issues, check for other causes — complex CSS animations can have a noticeable impact.