Get started

Engineering NotesVoice and meetings

A dropped meeting recording pauses the meeting instead of ending it

A meeting recording runs for an hour or more in a single browser tab. During that hour the Wi-Fi drops, the laptop goes to sleep, someone closes or refreshes the tab, a sign-in session expires, or the presenter shuts the laptop and tries to carry on from a phone. We delete the original audio once a meeting ends (“Ending a meeting drops the last remarks; audio waits for transcription”), so if the sound from the gap never reaches the server, or if we read the drop as the end of the meeting, that stretch cannot be recovered. And if two windows upload the same meeting at once, two devices' audio ends up on one timeline.

This post describes the design that protects the audio under those conditions. Recording chunks are stored in the browser first, a single lease decides which window is recording, an interrupted meeting is paused rather than ended and later continues on the same timeline, and sleep gaps are recorded as gaps.

The server can rarely tell why a recording stopped

A recording can stop for many reasons, but the server usually sees the same thing: chunks stop arriving.

Cause What the server sees
Network drop (recording continues) Chunks and polling stop
Tab closed or refreshed A close request sent right before the page leaves (sometimes it never arrives)
Force quit, power loss, browser crash Chunks and polling stop
Sleep or screen lock Silence for minutes, then chunks again
Sign-in expired Uploads are rejected with 401
Taken over on another device A second recording opens on the same meeting

Among meetings that have gone quiet, the server cannot pick out the ones that really ended. A person whose sign-in expired keeps talking, and a laptop waking from sleep starts sending chunks again minutes later. The simplest design holds chunks in memory, uploads them right away, and ends the meeting after some period without a signal. Under that design, chunks in memory vanish when the tab closes mid-upload, and the ended meeting moves to a final state that deletes the audio, so the person who signs back in has no way to keep recording. That is why a meeting ends only when someone clicks End meeting, or when it has stayed interrupted past the retention period.

Chunks go into browser storage first and are deleted once the server confirms them

The browser cuts the recording into a chunk every 5 seconds (“Overlapping meetings stall transcripts, so split queues by resource”). Each chunk goes into the browser's IndexedDB first, is then sent to the server, and is deleted only after the server confirms it. Send first and store later, and a chunk in flight when the tab closes exists nowhere. After a refresh, the next page load uploads whatever is left. The server writes each chunk to object storage as soon as it arrives (“Files that must survive a redeploy live only in object storage”).

This store holds only chunks that have not been uploaded yet. Even a five-minute network outage stays under 10 MB, unlike storing the whole meeting (460 MB for four hours), which would put it in the range where the browser clears site data when it runs short of space.

The server appends chunks in arrival order, so chunks are sent one at a time in recording order. A failed upload is handled according to what went wrong.

  • A network error or a 5xx never discards the chunk. Sending waits at that chunk until the connection returns, then resumes from the earliest one.
  • A 401 keeps the chunks and stops recording. After you sign in again, the remaining chunks upload.
  • A 409 saying the meeting has already ended, another device took over, or the recording length limit (4 hours by default) was reached stops recording.
  • Only a chunk that gets the same answer however often it is resent, such as a format or size rejection, is skipped, and the screen says so. Left at the front, it would keep every later chunk from uploading.

If recording kept going after a 401, people would talk believing they were being recorded while the server received nothing. When nothing has uploaded for 60 seconds, the screen shows "Not being saved to the server — check your connection. Recording is still in progress".

One 30-second lease decides which window records

We call one stretch of recording by one window holding the microphone a capture session, and opening a session also takes that meeting's recording lease. The lease belongs to a session that is not closed and whose last signal arrived within 30 seconds (meeting.captureLeaseTimeoutSeconds). It is the same mechanism Kubernetes uses for node heartbeats.

Two signals renew the lease: chunk uploads and the meeting screen's polling (1.5 seconds by default). Polling counts because people are still at the screen when the microphone input drops for a moment. The reverse also holds: in a background tab the browser slows the polling timer, but the recorder still delivers a chunk every 5 seconds. As long as either signal is alive, the lease does not expire.

While the lease is live, if another window tries to open recording on the same meeting, the server rejects it with a 409, and the screen shows "This meeting is being recorded in another window or device." with a Take over here button. Taking over transfers the lease immediately without waiting for it to expire. Someone moving to a phone because the laptop died has no reason to wait 30 seconds. The window that lost the lease gets a 409 on its next upload or poll, stops recording, and shows "Another window or device took over the recording." If that window could revive the same session by retrying, two devices' audio would mix on one timeline, so a window that records again always opens a new session.

Two tabs in the same browser are filtered out before either grabs the microphone, using the Web Locks API. With only the server lease, the second tab would get its 409 after it had already started recording, and people would talk in the meantime believing they were being recorded. The lock is tried once, without waiting. A waiting tab would switch the microphone on, forgotten, the moment the first tab finished.

An interrupted meeting is paused after 10 minutes and ended after 7 days

After 30 seconds without a signal, the lease expires and another window can open recording without taking over. The session itself stays open. When the network comes back, the window sends chunks and polls on the same session again, and recording continues without anyone clicking anything.

Recording lease and meeting state at 30 seconds, 10 minutes and 7 days after the last signal
Figure 1. Recording lease and meeting state at 30 seconds, 10 minutes and 7 days after the last signal

Every 5 minutes, a periodic job finds capture sessions with no signal for more than 10 minutes and closes them. The meeting stays in the recording state as a paused meeting, and its card in the list is labeled Stopped. A force quit, power loss or crash leaves no signal, so without this job the session would stay open. Whoever comes back opens the meeting and clicks Resume recording, or continues on another device. Closing or refreshing a tab closes the session right away with the request sent right before the page leaves, so the meeting pauses immediately instead of after 10 minutes.

Ten minutes is how long a returning window can keep recording without any action from the user. A window that comes back after its session was closed gets a 409 on its next poll and stops recording. Its queued chunks are still accepted. They are audio already recorded, with a known position in the meeting, so accepting them loses nothing and discarding them loses audio for good. They are rejected only when another device holds the lease, so they do not mix into that device's timeline. Accepting them does not revive the lease; whether to keep recording is the user's call through Resume recording.

Once a meeting has been paused for 7 days (PAUSED_MEETING_RETENTION_DAYS), it is ended. It follows the same path as a meeting someone ends by hand: the remaining audio is transcribed, the minutes are written, and the audio is deleted (“Ending a meeting drops the last remarks; audio waits for transcription”). A meeting that never ends would hold its original audio indefinitely, so this limit keeps the promise that original audio is not kept after a meeting ends, paused meetings included.

Resumed recording starts where the earlier sessions' recorded length ends

The browser sends only times relative to the start of its own capture session, which it counts as 0. The server fixes the session's position in the meeting once, when the session opens, and a new session starts at the sum of the lengths recorded by the meeting's earlier sessions. A refresh resets the browser's 0 without the timeline overlapping, and per-chunk upload delays never leak into positions.

Time spent between sessions does not count toward that sum. The break appears in the transcript as a single "Paused · {duration}" line. Gaps shorter than 3 seconds get no line, so session switches from a refresh or takeover do not add one. Each session's audio track is also stored separately, so one session's sound never mixes into another's track.

Three capture sessions, a sleep gap and a pause on one meeting timeline
Figure 2. Three capture sessions, a sleep gap and a pause on one meeting timeline

Sleep gaps are recorded as "Not recorded" rather than left blank

While recording, the desktop app turns on the operating system's sleep blocker, and the browser uses the Screen Wake Lock API to keep the screen on. The device still sleeps when the laptop lid closes, or while the tab is hidden and the browser has released the lock.

We detect a stopped device from the spacing of chunk arrivals. Chunks normally arrive every 5 seconds, background tabs included. If none arrive for more than 30 seconds and then they resume, the device was stopped in between. Measuring timer intervals instead would mistake a background tab's slowed timers for sleep.

On wake, the browser opens a new capture session and switches to it. It starts the new recorder first, so sound during the switch is kept, and those chunks wait in browser storage until the new session opens. The old session closes after its remaining chunks have uploaded.

The switch is needed because recording length is measured by the wall clock. The length keeps growing during sleep, so if uploads continued on the same session, the sleep stretch would look to the server like recording that has not arrived yet, even though it has no sound. The server would retry transcribing it for minutes, and ending the meeting would wait as long. Once the new session holds the lease, the server knows no more audio is coming for that stretch of the old session and immediately settles it as not recorded. The timeline stays on the wall clock, so speech after waking lands at its real time.

The screen shows "The device was asleep or locked for about {count} minutes, so nothing was recorded during that time", and the minutes mark that stretch as "Not recorded". Left blank, it would read to anyone going through the minutes as minutes in which nothing was decided. Stretches with no speech to transcribe are counted separately, since silence is not missing audio.

Design choices

We pause an interrupted meeting instead of ending it. Ending it is simpler and produces minutes right away. But the server cannot tell an interrupted meeting from a finished one, and ending one by mistake closes the way to resume for good. The cost is that a paused meeting holds its original audio, which is why there is a 7-day retention limit. Each person has one meeting in progress at a time, so starting a new meeting while one is paused opens the "There is a paused meeting" dialog with Resume and Finish and start new.

We store chunks in browser storage first rather than in memory. Memory is simpler, but chunks disappear with the tab. In browsers where IndexedDB is unavailable, such as those blocking site data, we fall back to memory, which protects chunks only while the tab stays open.

We always open a new session instead of reviving a closed one. The cost is that a window that lost the lease needs one more click to record again; in return, one timeline only ever holds one device's audio.

After sleep, we switch sessions instead of continuing the old one. Continuing is less code, but the sleep stretch holds up server retries and the meeting's end for minutes.

If you are building a long-running recording or upload in a browser tab, start by asking whether the signals the server receives can tell you why it stopped. If they cannot, pause the interrupted work rather than ending it, and leave the decision to end it to a person and a retention period. That is what prevents losses you cannot undo.