Skip to content
Kamau Wanyee

Recording is a distributed system

By ··
·8 min read
#posts#react-native#reliability#mobile

Recording on a phone looks local: press a button and receive a file. In practice, the operating system can interrupt capture, native audio can lose its route, disk can hold an incomplete file, and the network can drop during upload. The user may also change their intent before delivery finishes.

I worked on mobile recording and upload workflows with these failure modes. The important shift was to treat one phone as a small distributed system. The operating system reports interruptions, native audio reports capture facts, disk holds artifacts, and the network reports delivery. Each source can fail independently.

The consequence was human before it was technical. A recording could be the only durable trace of an event. Losing it meant losing work that the user could not recreate.

The single-file model

The first design usually follows this sequence: start a recorder, trust the recording flag, stop, treat the returned file as complete, upload it, and delete local state.

Each step produces a tempting signal. The recording flag describes application state. A file URI confirms that a path exists. An HTTP response confirms that bytes reached one endpoint. None of those signals establishes that the session produced usable media and completed the full delivery workflow.

Several failures expose the gap:

  • An interruption can leave a file with data but without the index written at close.
  • The JavaScript layer can believe that capture continues after the native audio session has changed state.
  • A process can die while session identity and segment paths exist only in memory.
  • Cleanup can release native resources or delete temporary paths before recovery state reaches disk.

Interruption belongs in the normal recording model. It needs a state, a durable checkpoint, and a defined next action.

Model the session

A session has a stable identity that survives interruption and process restart. The session contains an ordered sequence of segments. Each segment has its own identity and path. Session metadata records the order, current state, and references needed for recovery.

This model gives each layer a clear responsibility:

LayerResponsibility
Native audioReport samples, interruptions, route changes, and capture facts
Session stateApply policy to start, interrupt, resume, finalize, or abandon
DiskPreserve segments and metadata across process death
Upload workerDeliver artifacts with stable identity and recoverable progress

The state machine owns decisions. Native audio supplies observations. Disk holds the evidence. The upload worker handles delivery.

What the current audio library makes easier

The current mobile package uses react-native-audio-api. It handles several difficult native boundaries behind a small React Native interface:

  • AudioRecorder writes mono M4A output directly to the app document directory.
  • File output supports an Android flush interval and byte-based rotation, so long sessions become manageable segments.
  • AudioManager exposes permission checks, audio-session activation, interruptions, route changes, and ducking.
  • onAudioReady exposes PCM buffers for capture-health checks. The application uses them to detect flatline input, repeated buffers, clipping, static, silence, and usable signal.
  • Stop results include paths, duration, and size. getAudioDuration provides a second duration check, and concatAudioFiles joins rotated segments after capture.

That removes a large amount of platform-specific plumbing. The application can focus on session identity, state transitions, preservation, validation, and upload recovery.

The library supplies audio primitives. The application still owns interruption policy, orphan recovery, cleanup ordering, and user-visible completion. The distributed-systems work remains in the application.

Preserve before native teardown

An interruption forces the microphone to stop or change state. The recording session can remain open while the application preserves its evidence. Those operations need separate ordering.

The safe sequence is:

interrupt
  -> preserve the current segment
  -> persist session and segment metadata
  -> release required native resources
  -> resume with the same session identity

The application can then open a new segment under the existing session. The interrupted native object can be discarded while the recording identity remains stable.

This ordering also changes cleanup. Releasing the recorder, deactivating the audio session, clearing temporary paths, and resetting the UI are separate actions. The state machine should reach a durable next state before destructive cleanup begins.

Keep local capture ahead of the network

Local persistence and live delivery can share captured audio. I extended the React Native path with Kotlin and Swift components for local persistence.

The current product uses local-first capture for visit recordings. A network-backed mode serves correction recordings and can provide live transcription. The two paths share the same session policy and local recovery concerns. A stream can be delayed, interrupted, or unavailable while local capture continues. Network availability should influence delivery state, and local storage should decide whether the recording exists.

Repair media before declaring completion

An incomplete media file is recovery input. A player error leaves its fate open.

When the user stops, the application preserves the final open segment, attempts repair or remux, and retains the original segments if repair fails. The repaired artifact then enters the upload workflow with its session identity and artifact references intact.

The upload worker also needs durable checkpoints. A retry keeps the same identity, paths, and progress. On boot, the worker reconciles disk state with memory. If the process died during an upload, the worker rebuilds its work from disk. An empty in-memory queue carries no evidence that the session finished.

On launch, the app scans for orphaned segments, validates them, preserves recoverable files, and refreshes stored duration. Recovery can happen before the user reopens the original workflow.

Stable identity makes duplicate delivery manageable and protects local intent during retries.

Repair on the server

An interrupted M4A can arrive with its raw AAC frames still present in the mdat atom while its moov atom is missing. The moov contains the track and index metadata that demuxers need. The audio data survives, but ordinary FFmpeg remuxing cannot open the file.

The server pre-validates every uploaded segment. For a moov-less file, it chooses a healthy sibling from the same batch as a reference. The sibling uses the same encoder profile, so a reference-based repair can reconstruct the missing metadata. A single damaged segment can use a canonical reference with the matching profile. The recovered file is validated again before the server merges the segments.

That distinction changes the recovery decision. “Corrupt” can mean “metadata missing,” not “audio absent.” The server still fails safely when reconstruction fails, but it gives persisted audio one more chance to become a usable recording.

The architecture still needs an exit when its cost exceeds its value. Sad I had to kill a technically cool feature explains why I removed a live path that made dependable capture harder to trust.

Verify each finish line

“The file exists” is one check among several. Recovery needs to distinguish:

an existing path, usable media, a repaired final artifact, staged upload, remote acknowledgement, and safe local deletion.

These states should remain visible in metadata and, where useful, in the product interface. A session can wait for repair, connectivity, or remote acknowledgement. Stop diagnostics record file size, native duration, validated duration, validity, and failure reason.

The product also needs an explicit meaning for completion. A successful first network request may indicate that bytes were accepted. Processing, acknowledgement, and local retention policy can still be pending. The local artifact stays until the defined finish line is real.

Tradeoffs and the loss window

Durability uses storage. Retaining segments increases storage pressure and complicates retention and privacy policies. Cleanup follows a confirmed terminal state.

Repair introduces platform-specific complexity. Some containers are easy to inspect and difficult to finalize after abrupt termination. Retaining evidence is safer, even when recovery requires a later repair step.

Native lifecycle behavior affects user trust. A foreground service and persistent notification can improve Android survival, while confusing notification behavior can cause users to stop or dismiss capture.

Some audio remains unrecoverable. A process can die while data is still in a native or operating-system buffer. The design can shorten that loss window and expose the remaining uncertainty. It cannot reconstruct samples that never reached durable storage.

The practical rule

For any product that captures irreversible audio, give sessions stable identity, store evidence before cleanup, make recovery inspectable, and define success beyond the first optimistic response. The recorder API supplies start() and stop(). The surrounding system earns trust by preserving partial work, recovering after interruption, and stating clearly what cannot be recovered.