Recording start takes ~3 seconds and stop takes ~20–23 seconds from the moment the moderator clicks the button to the moment the UI updates and the voice announcement plays.
Environment
Web client: Next.js + @livekit/components-react
Recording: startRoomCompositeEgress → S3
Full Process Breakdown
START (~3 seconds)
Moderator clicks “Start Recording”│├─ fetch /api/record/start│ ├─ POST to external API (get user S3 folder) ~500ms–1s│ ├─ egressClient.listEgress() (duplicate check) ~300ms│ ├─ egressClient.startRoomCompositeEgress() ~1–2s ← LiveKit spins up compositor│ ├─ redis.setex() store start time ~50ms│ └─ roomService.sendData(RECORDING_STATUS_CHANGED, true)│├─ All clients receive data message → UI updates instantly└─ LiveKit fires RecordingStatusChanged ~1–2s later (redundant)
Start is already well optimised. The ~3s is mostly LiveKit’s own compositor spin-up time — nothing we can cut.
STOP (~20–23 seconds) — the real problem
This is where the delay was coming from. There were three separate layers each adding time:
Layer 1 — Our stop API route (artificial +2s)
Moderator clicks “Stop Recording”│└─ fetch /api/record/stop├─ egressClient.listEgress() ~300ms├─ await setTimeout(2000ms) ← ARTIFICIAL DELAY (removed)├─ egressClient.stopEgress() ~500ms└─ roomService.sendData(recording-stopped)
We had a 2 second intentional delay before calling stopEgress() to “allow transcript association”. This was unnecessary — transcript association happens in the webhook, not here.
Layer 2 — LiveKit egress winds down (unavoidable ~15–20s)
stopEgress() called│└─ LiveKit finalises the MP4 file├─ Flushes video buffer├─ Closes the egress compositor├─ Uploads final MP4 to S3└─ Fires egress_ended webhook ← only after S3 upload complete
This is entirely on LiveKit’s infrastructure side. We cannot speed this up.
Layer 3 — Our webhook pipeline (artificial +8s)
egress_ended webhook received│├─ await setTimeout(3000ms) ← waiting for transcripts (reduced to 500ms)├─ fetch transcript from Redis├─ store recording segment│└─ maybeFinalizeMeeting()├─ await setTimeout(5000ms) ← finalization guard (reduced to 1s)├─ listParticipants()├─ listEgress()└─ sendSessionRecordingsToExternalAPI()
Two more artificial delays totalling 8 seconds inside the webhook processing.
Layer 4 — UI was waiting for the wrong event
stopEgress() called│└─ RecordingStatusChanged event fires on all clients└─ ONLY fires after LiveKit fully winds down egress (~15–20s)└─ RecordingIndicator used useIsRecording() hook└─ hook is bound to RecordingStatusChanged└─ UI updates only after ~20s ← ROOT CAUSE
This was the biggest problem. useIsRecording() from @livekit/components-react only reacts to LiveKit’s native RecordingStatusChanged event — which LiveKit fires only after the egress fully winds down and the MP4 is finalised on S3. Our sendData message was being sent but RecordingIndicator was completely ignoring it because it was reading from useIsRecording() internally.
Total Artificial Delay Breakdown
Source Delay Statusstop/route.ts — setTimeout before stopEgress +2s Removedlivekit-webhook — setTimeout before transcript fetch +3s Reduced to 500mslivekit-webhook — setTimeout in maybeFinalizeMeeting +5s Reduced to 1sRecordingIndicator using useIsRecording() +15–20s Fixed — now prop-drivenTotal removed ~25s
Questions
Is RecordingStatusChanged intentionally fired only after egress file finalisation, or is this a side effect?
Is there a recommended way to get an immediate acknowledgement signal when stopEgress() is accepted — separate from when the file is ready?
“Our current workaround uses optimistic state via sendData — but if stopEgress() fails silently, the UI shows stopped while recording is still active. We want to know if there is a safer official approach.”
in this have we metioned about transcripts also do we need to?