Oct 19, 2026 · 6 min read

Why I Removed FFmpeg from My Audio AI Backend

#aws#architecture#audio#node#performance

How questioning a "standard" architecture decision eliminated 100% CPU spikes, stopped generation loss, and rescued a 1 GB AWS instance from the Linux OOM Killer.

1. The Setup: Running on the Razor’s Edge

I built Murmur—a voice-first personal thought notebook and conversational memory app. The user experience is designed to be frictionless: you hold a mic pill, speak unscripted thoughts, and release. In the cloud, an asynchronous AI pipeline powered by OpenAI Whisper (whisper-1) and GPT-4o-mini transcribes the speech, extracts actionable tasks with deadlines, categorizes spontaneous ideas and decisions, and computes vector embeddings for semantic recall.

Behind the scenes, my backend runs on a lean setup:

  • Host: A single AWS t3.micro instance in Mumbai (ap-south-1).
  • RAM: Exactly 1 GB (1,024 MB).
  • CPU: 2 burstable vCPUs (20% baseline performance).
  • Neighbors: It shares this single instance with native PostgreSQL 18, Redis, Nginx, and two other active side projects.

On paper, 1 GB of memory is manageable for an early-stage beta if your code is efficient. In reality, my server was walking on the edge of a cliff.

Every time a user recorded a 2-minute voice note, the CPU graph spiked vertically to 100%. CPU burst credits began rapidly draining toward zero. If two users uploaded a thought within the same minute, server memory surged past 1 GB, disk swap began thrashing, and Linux’s dreaded Out-Of-Memory (OOM) Killer was moments away from terminating PostgreSQL or my Node.js process.

The problem wasn't database locks. It wasn't OpenAI API latency.

The culprit was FFmpeg.


2. The "Default Architecture" Trap

When building any audio or voice application, almost every tutorial, framework template, and blog post recommends the exact same pattern:

"Mobile clients record in all kinds of chaotic formats (AAC, WAV, OGG, M4A). You should normalize everything to standard 128 kbps MP3 on your server before storing it."

Following this convention, I added fluent-ffmpeg and ffmpeg-static to my Node.js backend.

Here is the absurd "Rube Goldberg machine" my backend executed on every single spoken thought:

Rendering architecture diagram...

Pause and examine that workflow:

  1. The client uploaded bytes to cloud storage.
  2. The server downloaded those bytes back into precious RAM.
  3. The server executed disk I/O to write a temporary file.
  4. A heavy C binary (ffmpeg) was spawned, pinning the CPU core and allocating native memory buffers.
  5. The server executed more disk I/O to read the transcoded MP3.
  6. The server made another outbound network call to re-upload to Cloudflare R2.
  7. Finally, the worker downloaded the MP3 yet again to post to OpenAI Whisper.

On a multi-core dedicated server with 32 GB of RAM, you might not notice this waste. But on an AWS t3.micro, running FFmpeg on the API server was like revving an engine in neutral until the radiator overheats.


3. The Epiphany: Four First-Principles Questions

One night while inspecting server metrics and dreading the bill for a larger EC2 instance, I stopped and asked four fundamental questions:

Question 1: What does my mobile app actually record?

In Flutter, the app uses the record package configured with AudioEncoder.aacLc at 128 kbps and 44.1 kHz, written into an .m4a file. AAC (Advanced Audio Coding) is the native hardware standard on both Android and iOS. It delivers better acoustic fidelity than MP3 at identical bitrates.

Question 2: What formats does OpenAI Whisper actually accept?

I checked the official OpenAI Audio API specification:

"File uploads are currently limited to 25 MB and the following input file types are supported: mp3, mp4, mpeg, mpga, m4a, wav, and webm."

Whisper natively accepts .m4a directly. It does not need MP3.

Question 3: What formats can mobile audio players stream?

In the Flutter client, playback is handled by just_audio (backed by ExoPlayer on Android and AVPlayer on iOS). Both native operating system engines decode AAC inside an .m4a container with built-in hardware acceleration.

Question 4: Does Cloudflare R2 care what bytes it stores?

Cloudflare R2 is an S3-compatible object store. To R2, a byte is a byte. Whether an object key ends in .mp3 or .m4a makes zero difference to durability, latency, or storage costs.


4. The Realization

I was burning CPU cycles, exhausting memory, and thrashing server disk solely to convert native M4A into MP3 because of an outdated industry reflex.

Worse still: transcoding an AAC file into MP3 introduced generation loss. You are decoding a lossy format (AAC) and re-encoding it into an older lossy format (MP3). This subtly degraded speech clarity right before sending the audio into an AI transcription model.


5. The Fix: Zero-Transcoding Architecture

I deleted fluent-ffmpeg and ffmpeg-static from package.json.

The new architecture is lean and direct:

Rendering architecture diagram...

Handling Backward Compatibility (Zero Downtime)

In production, you can't just break existing data:

  1. Existing Memos: Memos previously converted to .mp3 stay in R2 as .mp3. The playback player and Whisper handle both .mp3 and .m4a interchangeably based on the file extension.
  2. In-Flight Client Sessions: If an older client app version uploads to the legacy temporary prefix (uploads/{userId}/...), the server uses AWS S3’s native CopyObjectCommand to execute a server-side copy within Cloudflare R2, followed by DeleteObjectCommand. The audio is moved in 10ms without downloading a single byte to the Node.js server.
// Clean server-side move within Cloudflare R2 (zero memory overhead)
if (key.startsWith(`uploads/${uid}/`)) {
  const ext = _guessExt(key) || 'm4a';
  finalKey = this.storageService.buildKey(uid, ext);
  await this.storageService.copyObject(key, finalKey);
  await this.storageService.deleteObject(key);
}

6. The Results & Benchmarks

MetricBefore (With FFmpeg)After (Zero-Transcoding)Improvement
API Server CPU Spike90% – 100% per upload0.5% – 2% (I/O only)~98% reduction
Peak RAM per Upload+70 MB to +100 MB0 MB (Direct-to-R2)100% memory saved
Disk I/O2 disk writes/reads per memo0 bytes written to diskZero disk wear
Upload Processing Latency2.5s – 5.5s (conversion lag)120ms (DB insert & queue)~25× faster
node_modules Disk Size~140 MB (ffmpeg-static binary)Reduced by ~95 MB68% smaller bundle
Transcription AccuracyLossy transcode artifactsRaw microphone fidelityCleaner STT clarity

7. Key Lessons for Backend & AI Engineers

  1. Question the "Normalization" Reflex:
    Before converting files on your backend, verify if the downstream consumers (Whisper, Gemini, Deepgram, native mobile players) actually require it. Modern AI models accept modern audio formats out of the box.
  2. Push Heavy Lifting to the Edges:
    The user's smartphone already has dedicated hardware audio encoders (DSP). It produces perfectly compressed AAC with zero battery drain. Transcoding that audio again on a $7 cloud VM is an anti-pattern.
  3. Use Storage Primitives:
    Cloudflare R2 and AWS S3 have presigned uploads and server-side CopyObject primitives. Let storage infrastructure manage file movement instead of proxying megabytes through your Node.js event loop.
  4. The Best Code is the Code You Delete:
    Removing two dependencies and 100 lines of complex child-process handling made the system faster, more reliable, and completely stable on the smallest cloud instance available.

Engineering is often about stripping away conventional complexity. Before scaling up infrastructure or adding worker queues to handle heavy workloads, check if the work needed to exist in the first place.