> ## Documentation Index
> Fetch the complete documentation index at: https://docs.daily.co/llms.txt
> Use this file to discover all available pages before exploring further.

# Recording

> Use startRecording(), stopRecording(), and updateRecording() to control Daily call recordings from daily-js.

<Card title="Recording overview" icon="video" href="/docs/guides/features/recording">
  New to Daily recording? Start here — covers recording types, when to use each, how to enable recording on a room or meeting token, and how to retrieve recordings.
</Card>

Recording must be enabled on the room or meeting token (via `enable_recording`) before `startRecording()` will work. See the overview guide above for setup.

## Starting a recording

[`startRecording()`](/reference/daily-js/instance-methods/start-recording) begins a recording with optional configuration:

```javascript theme={null}
// Start a cloud recording with defaults
call.startRecording();

// Start with explicit options
call.startRecording({
  type: 'cloud',
  width: 1920,
  height: 1080,
  fps: 30,
  videoBitrate: 4000,
  audioBitrate: 128,
  backgroundColor: '#000000',
});
```

## Recording options

<ParamField path="type" type="'cloud' | 'raw-tracks' | 'local' | 'cloud-audio-only'">
  The recording mode. Defaults to whichever type is enabled on the room or token. See the [recording overview](/docs/guides/features/recording) for a description of each type and when to use it.
</ParamField>

<ParamField path="width" type="number">
  Output video width in pixels. Default: `1280`.
</ParamField>

<ParamField path="height" type="number">
  Output video height in pixels. Default: `720`.
</ParamField>

<ParamField path="fps" type="number">
  Frames per second. Default: `30`.
</ParamField>

<ParamField path="videoBitrate" type="number">
  Video bitrate in kbps.
</ParamField>

<ParamField path="audioBitrate" type="number">
  Audio bitrate in kbps.
</ParamField>

<ParamField path="minIdleTimeOut" type="number">
  Seconds of idle time (no active streams) before the recording automatically
  stops. Default: `300`.
</ParamField>

<ParamField path="maxDuration" type="number">
  Maximum recording duration in seconds.
</ParamField>

<ParamField path="backgroundColor" type="string">
  Background color for the composed output when participant tiles do not fill
  the frame. Accepts a CSS hex color string, e.g. `'#1a1a2e'`.
</ParamField>

<ParamField path="instanceId" type="string">
  Identifier for this recording instance. Required when running multiple
  simultaneous recordings. See [multiple simultaneous recordings](#multiple-simultaneous-recordings).
</ParamField>

<ParamField path="layout" type="DailyStreamingLayoutConfig">
  Controls how participant tiles are arranged in the composed output. See [`DailyStreamingLayoutConfig`](/reference/daily-js/types/daily-streaming-layout-config) for the full type reference, or [layout presets](#layout-presets) below for usage examples.
</ParamField>

## Layout presets

The `layout` option controls the visual composition of cloud recordings. See [`DailyStreamingLayoutConfig`](/reference/daily-js/types/daily-streaming-layout-config) for field-level documentation on each preset. For screenshots, see the [recording overview](/docs/guides/features/recording#customize-cloud-recording-layouts).

### `default`

```javascript theme={null}
call.startRecording({
  layout: {
    preset: 'default',
    max_cam_streams: 9,         // maximum cameras shown
    participants: {
      video: ['session-id-1'],  // specific participant always shown
      sort: 'active',           // sort by speaking activity
    },
  },
});
```

### `single-participant`

```javascript theme={null}
call.startRecording({
  layout: {
    preset: 'single-participant',
    session_id: 'abc123',  // required: which participant to show
  },
});
```

### `active-participant`

```javascript theme={null}
call.startRecording({
  layout: { preset: 'active-participant' },
});
```

### `portrait`

```javascript theme={null}
call.startRecording({
  layout: {
    preset: 'portrait',
    variant: 'inset',  // 'vertical' (default) or 'inset'
    max_cam_streams: 4,
  },
});
```

### `audio-only`

```javascript theme={null}
call.startRecording({
  type: 'cloud-audio-only',
  layout: { preset: 'audio-only' },
});
```

### `custom`

Use Daily's VCS baseline composition to fully control the layout programmatically — modes, overlays, labels, participant ordering, and more. The [`startRecording()` reference](/reference/daily-js/types/daily-streaming-layout-config#baseline-composition-properties) documents all available `composition_params`.

```javascript theme={null}
call.startRecording({
  layout: {
    preset: 'custom',
    composition_id: 'daily:baseline',
    composition_params: {
      mode: 'dominant',
      'videoSettings.showParticipantLabels': true,
    },
    session_assets: {
      'images/logo.png': 'https://example.com/logo.png',
    },
  },
});
```

### `participants`

Several presets accept a `participants` field to filter which participants' video and audio are included in the recording. See [`DailyStreamingLayoutConfig`](/reference/daily-js/types/daily-streaming-layout-config#participants) for the full field list.

<Note>
  You must resend `participants` on every `updateRecording()` call — it is not persisted from the previous call. If omitted on update, all participants will be included.
</Note>

## Updating layout during recording

Call [`updateRecording()`](/reference/daily-js/instance-methods/update-recording) to switch layouts while a recording is in progress, without stopping and restarting:

```javascript theme={null}
// Start in default grid layout
call.startRecording({
  layout: { preset: 'default' },
});

// Later, switch to follow the active speaker
call.updateRecording({
  layout: { preset: 'active-participant' },
});
```

## Stopping a recording

[`stopRecording()`](/reference/daily-js/instance-methods/stop-recording) ends the active recording:

```javascript theme={null}
call.stopRecording();
```

## Multiple simultaneous recordings

Pass a unique `instanceId` to run more than one recording at the same time — for example, two cloud recordings with different layouts for different audiences:

```javascript theme={null}
// Portrait layout for mobile
call.startRecording({
  instanceId: 'portrait',
  type: 'cloud',
  layout: { preset: 'portrait', variant: 'vertical' },
});

// Landscape layout for desktop
call.startRecording({
  instanceId: 'landscape',
  type: 'cloud',
  layout: { preset: 'active-participant' },
});

// Update and stop each independently
call.updateRecording({ instanceId: 'landscape', layout: { preset: 'default' } });
call.stopRecording({ instanceId: 'portrait' });
call.stopRecording({ instanceId: 'landscape' });
```

<Note>
  Multiple instances work best when each runs the same recording type. Mixing types (e.g., `cloud` + `raw-tracks` simultaneously) is possible but not a recommended pattern — if you need both, contact [Daily support](https://www.daily.co/contact/support) to discuss your use case. For full details on instance limits and billing, see the [multi-instance guide](/docs/guides/features/live-streaming/multi-instance).
</Note>

## Recording events

Full payload details for all recording events are in the [recording events reference](/reference/daily-js/events/recording-events).

### `recording-started`

```javascript theme={null}
call.on('recording-started', (event) => {
  console.log('Recording started:', event.recordingId);
  console.log('Type:', event.type);           // 'cloud' | 'raw-tracks' | ...
  console.log('Started by:', event.startedBy); // session ID
  console.log('Layout:', event.layout);
});
```

### `recording-stopped`

```javascript theme={null}
call.on('recording-stopped', () => {
  console.log('Recording stopped.');
});
```

### `recording-error`

```javascript theme={null}
call.on('recording-error', ({ errorMsg }) => {
  console.error('Recording error:', errorMsg);
});
```

## Retrying failed recording starts

A recording start can fail for transient reasons, like a timeout while the recording infrastructure spins up. Sometimes the start command doesn't reach the recording server at all due to client network conditions, so `startRecording()` never fires a `recording-error` event: the recording just never starts.

Because of this, don't rely on the error event alone. After you call `startRecording()`, wait for the `recording-started` event. If it doesn't arrive within a short timeout, call `startRecording()` again. Use exponential backoff between attempts (wait a bit, retry, then double the wait) and cap the number of attempts so you don't retry forever.

```javascript theme={null}
const MAX_ATTEMPTS = 4;
const START_TIMEOUT_MS = 5000; // how long to wait for 'recording-started'
let attempt = 0;
let startTimer = null;

function startRecordingWithRetry() {
  attempt += 1;
  call.startRecording({ type: 'cloud' });

  // If 'recording-started' doesn't arrive, the command may not have reached
  // the recording server, so try again.
  startTimer = setTimeout(() => retryOrGiveUp(), START_TIMEOUT_MS);
}

function retryOrGiveUp(errorMsg) {
  if (attempt >= MAX_ATTEMPTS) {
    console.error(
      'Recording failed after retries:',
      errorMsg || 'no recording-started event'
    );
    return;
  }
  const delayMs = 1000 * 2 ** (attempt - 1); // 1s, 2s, 4s
  setTimeout(startRecordingWithRetry, delayMs);
}

call.on('recording-started', () => {
  clearTimeout(startTimer); // it started, so stop waiting
  attempt = 0; // reset for the next recording
});

call.on('recording-error', ({ errorMsg }) => {
  clearTimeout(startTimer);
  retryOrGiveUp(errorMsg);
});

startRecordingWithRetry();
```

If you start recordings from your server instead, apply the same pattern to the [`POST /rooms/:name/recordings/start`](/reference/rest-api/rooms/recordings/start) REST endpoint:

```python theme={null}
import time
import requests

MAX_ATTEMPTS = 4

def start_recording(room_name):
    url = f"https://api.daily.co/v1/rooms/{room_name}/recordings/start"
    headers = {"Authorization": "Bearer DAILY_API_KEY"}
    delay = 1  # seconds

    for attempt in range(MAX_ATTEMPTS):
        response = requests.post(url, headers=headers, timeout=30)
        if response.ok:
            return response.json()
        # Sleep only between attempts, not after the last one.
        if attempt < MAX_ATTEMPTS - 1:
            time.sleep(delay)  # 1s, 2s, 4s
            delay *= 2

    raise RuntimeError("recording did not start after retries")
```

<Note>
  Only retry transient errors. If the error says recording is not enabled on the room or token, fix the configuration instead of retrying.
</Note>

## Complete cloud recording example

```javascript theme={null}
const call = Daily.createCallObject();
await call.join({ url: 'https://your-domain.daily.co/room' });

// Start a 1080p cloud recording with active-speaker layout
call.startRecording({
  type: 'cloud',
  width: 1920,
  height: 1080,
  fps: 30,
  layout: { preset: 'active-participant' },
});

call.on('recording-started', ({ recordingId }) => {
  console.log('Recording ID:', recordingId);
  recordingBadge.hidden = false;
});

call.on('recording-stopped', () => {
  recordingBadge.hidden = true;
});

call.on('recording-error', ({ errorMsg }) => {
  console.error('Recording failed:', errorMsg);
});

// Switch layout mid-recording when a screen share starts
call.on('local-screen-share-started', () => {
  call.updateRecording({
    layout: {
      preset: 'single-participant',
      session_id: call.participants().local.session_id,
    },
  });
});

call.on('local-screen-share-stopped', () => {
  call.updateRecording({
    layout: { preset: 'active-participant' },
  });
});

document.getElementById('stop-recording').onclick = () =>
  call.stopRecording();
```


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.