◉RORK LABJP
●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SDK 58 — Beta since 09/15 with no stable date yet; running projects in Expo Go has required login since 09/03●WIDGET — A write-up traces an iOS widget that renders black only in production builds●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SDK 58 — Beta since 09/15 with no stable date yet; running projects in Expo Go has required login since 09/03●WIDGET — A write-up traces an iOS widget that renders black only in production builds●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…
Articles/Dev Tools
⬡ Dev Tools/2026-08-31Intermediate

Three Minutes After the Screen Locked, My expo-audio Ambient Sound Went Silent

Why expo-audio stops playing when the screen locks, diagnosed as three separate layers: build config, audio session, and lock screen integration. The three-minute stop on Android is documented behavior.

expo-audio4background audio3React Native239Expo219Android55

I run ambient sound apps for sleep in my own projects. A user taps play on rain sounds and drifts off; the screen locks within a minute, and only the audio is supposed to remain until morning. For this kind of app, everything that matters happens after the screen goes dark.

While testing a build that had migrated from expo-av to expo-audio, I ran into a strange failure mode on a physical Android device. The audio kept playing right after the screen locked — then, roughly three minutes later, it stopped. No error, no crash. Just silence.

If playback dies the instant you lock the screen, you suspect a missing setting. But "plays for a while, then stops" is much harder to reason about. It turned out this wasn't a bug or a quirk of my test device. It's a stopping behavior written plainly into Expo's documentation. Here is how I traced it, and how I now think about background audio as three separate layers.

Your expo-av knowledge does not carry over to expo-audio

Some context first. expo-av was deprecated in SDK 52, and Expo's docs announced its removal in SDK 55. Since apps generated with Rork are built on Expo, rebuilding on a current SDK means your audio code lives in expo-audio now, whether you planned the migration or not.

In the expo-av era, background playback hinged on a single boolean: staysActiveInBackground, passed to setAudioModeAsync. Set it, and you were mostly done.

That field does not exist in expo-audio's AudioMode. You search for the name, fail to find it, spot the plausible-looking shouldPlayInBackground, set it to true, and move on — which is exactly what I did. On iOS, that works. On Android, your audio stops after about three minutes.

What changed is that one boolean became three layers. That structural shift is the real trap.

The three layers, and how each one fails

Here is the map I wish I'd had. Background playback in expo-audio only works when all three of these layers are in place.

LayerWhere it livesWhat to setSymptom when missing
1. Build configConfig plugin in app.jsonenableBackgroundPlayback (default true)iOS stops instantly on lock; Android lacks the service declaration
2. Audio sessionsetAudioModeAsync at runtimeshouldPlayInBackground: true (default false)Both platforms stop immediately on lock or backgrounding
3. Lock screen integrationsetActiveForLockScreen at runtimeEffectively required on Android (optional on iOS)Android only: playback stops after roughly 3 minutes

The table doubles as a reverse lookup. Audio dies the moment the screen locks: check layers one and two. Audio plays for a while and then goes quiet: layer three. The three-minute mark is not noise — it's a fingerprint telling you which layer to inspect.

Layers one and two: the build-time declaration and the runtime session

Layer one is the config plugin. In app.json:

{
  "expo": {
    "plugins": [
      ["expo-audio", { "enableBackgroundPlayback": true }]
    ]
  }
}

This adds two Android permissions — FOREGROUND_SERVICE and FOREGROUND_SERVICE_MEDIA_PLAYBACK — plus the media playback foreground service declaration (AudioControlsService) to your manifest, and adds the audio entry to UIBackgroundModes on iOS. You never touch the native files yourself.

The subtle part: enableBackgroundPlayback defaults to true, so this layer is usually fine without you doing anything. Meanwhile, layer two's shouldPlayInBackground defaults to false. The two defaults point in opposite directions. That's why "the manifest has everything, yet the audio still stops" is such a common state of confusion during this migration.

Layer two is the session setup, called once before playback starts:

import { setAudioModeAsync } from 'expo-audio';
 
await setAudioModeAsync({
  playsInSilentMode: true,      // keep playing under the iOS silent switch
  shouldPlayInBackground: true, // the core switch; defaults to false
  interruptionMode: 'doNotMix', // required for lock screen controls
});

The default interruptionMode is 'mixWithOthers', but the docs are explicit that lock screen controls need 'doNotMix' — otherwise the OS may not associate those controls with your player. Also note that the old interruptionModeAndroid is deprecated; interruptionMode now applies to both platforms. Older tutorials will steer you toward field names that no longer do what they once did.

Layer three: without setActiveForLockScreen, Android stops at about three minutes

This is the heart of the matter. Expo's documentation says that on Android you have to enable lock screen controls with setActiveForLockScreen for sustained background playback; otherwise the audio stops after approximately three minutes of background playback (an OS limitation, not a bug to be fixed on your side).

My build stopping at the three-minute mark was this sentence playing out verbatim. The fix is to activate lock screen controls when playback starts:

player.loop = true;
player.setActiveForLockScreen(true, {
  title: 'Rain Sounds',
  artist: 'Ambient Sounds',
});
player.play();

This call posts a media notification, and the foreground service tied to that notification is what keeps playback alive. The notification in the shade is your visible proof that the audio will survive. Put the other way around: if you lock the screen and there's no media notification, your playback is sitting on a three-minute hourglass.

One design note: only one player can control the lock screen at a time, as the docs also state. In an ambient app that switches between multiple sound sources, you either re-activate the new player on each switch or keep one active player and refresh the display with updateLockScreenMetadata.

The minimal working setup, and why my test checklist now includes three minutes

All three layers in one place:

import { useAudioPlayer, setAudioModeAsync } from 'expo-audio';
import { useEffect } from 'react';
import { Button } from 'react-native';
 
const rainSound = require('./assets/rain.mp3');
 
export function AmbientPlayer() {
  const player = useAudioPlayer(rainSound);
 
  useEffect(() => {
    setAudioModeAsync({
      playsInSilentMode: true,
      shouldPlayInBackground: true,
      interruptionMode: 'doNotMix',
    });
  }, []);
 
  const start = () => {
    player.loop = true;
    player.setActiveForLockScreen(true, {
      title: 'Rain Sounds',
      artist: 'Ambient Sounds',
    });
    player.play();
  };
 
  return <Button title="Play" onPress={start} />;
}

This incident also added one step to my release checklist: start playback on a physical Android device, lock the screen, confirm the media notification is showing, and then keep listening for more than three minutes. A thirty-second check in a simulator will never surface this problem.

For a sleep app, three minutes is a brutal number. The audio dies at the exact moment the user is most defenseless — half asleep. And the review they leave will only say "the sound stops"; nobody writes down the three-minute detail that would point you at the cause. Waiting out those three minutes by hand is unglamorous testing, but it protects more than it appears to.

For a long time my own check was "lock the screen, listen for thirty seconds." The results were not good: I was missing, on my own desk, exactly the class of bug that only shows up after release. These days I keep a timestamped note instead:

ElapsedWhat to checkIf it fails here
Right after lockingAudio playing; media notification in the shadeLayer 1 or 2 (missing setting)
1–2 minutesNotification still presentIf it vanished, re-check the order of your layer-3 call
Just past 3 minutesAudio still playingLayer 3: setActiveForLockScreen was never called
10 minutesAudio and notification both still thereLook at other causes, such as battery optimization

One thing I haven't been able to confirm: on Android 13 and later, the notification permission state can change what you see in the shade, so I run these checks with notifications allowed. I haven't tested what happens on a device where the user has denied them — if that matters for your app, I'd check on a real device.

Ship on "still playing after three minutes," not on "playing." For sleep audio, that's a line I don't plan to move.

Other ambient-audio pitfalls — gapless loop seams, silence under the ring/silent switch — are collected in Three Things I Got Stuck on When Implementing Ambient Sound in a Healing App. And once lock-screen playback is solid, the next stage is timing: my premium article My Rork sleep timer faded out on time — but the sound didn't covers designing fades driven by playback position rather than wall-clock time.

Start with three minutes on a real device. If your audio survives that, the foundation for playing until morning is in place.

Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

⬡ Dev Tools2026-09-10
I Added a Dim Screen Button, and iPhones Stayed Dark After the App Was Closed
In expo-brightness, setBrightnessAsync applies only to the current activity on Android, but changes the device brightness itself on iOS. Here is why the cleanup burden falls on one platform only, and a hook that restores brightness through AppState.
⬡ Dev Tools2026-05-05
Taking a Rork Podcast App to Production — Migrating to expo-audio, Recovering from Interruptions, and Resumable Downloads
Implementation notes for making a Rork podcast app survive real use: migrating off expo-av before SDK 55 removes it, recovering from call interruptions, resuming broken downloads, and budgeting storage against iCloud backup.
⬡ Dev Tools2026-10-07
Japanese Headings Won't Turn Bold on Android: How to Trace and Fix a fontWeight That Does Nothing
A troubleshooting note for Rork apps where headings look bold on iPhone but stay thin on Android. How to tell the symptoms apart, pick weights through fontFamily, and verify the fix.