RORK LABJP
DEADLINE — From today, August 31, new submissions to Google Play and updates to existing apps must target Android 16, API level 36 or higherSCOPE — Existing apps are not removed. But anything below the requirement stops appearing for users on newer Android versions, so it does not vanish, it just stops reaching peopleEXTENSION — If you cannot make it, an extension can be requested through November 1. Every year some developers reach the deadline without knowing that option existedMIGRATION — What Rork generates sits on React Native and Expo, so raising targetSdkVersion always drags the Expo SDK and its native dependencies along with itTESTING — The time really goes after the build passes. Background execution limits, the permission model, and foreground service type declarations all take effect at onceORDER — A workable sequence: check whether an extension applies, plan the Expo SDK upgrade, then run device regression tests on a minimal build. Doing all three at once hides the causeDEADLINE — From today, August 31, new submissions to Google Play and updates to existing apps must target Android 16, API level 36 or higherSCOPE — Existing apps are not removed. But anything below the requirement stops appearing for users on newer Android versions, so it does not vanish, it just stops reaching peopleEXTENSION — If you cannot make it, an extension can be requested through November 1. Every year some developers reach the deadline without knowing that option existedMIGRATION — What Rork generates sits on React Native and Expo, so raising targetSdkVersion always drags the Expo SDK and its native dependencies along with itTESTING — The time really goes after the build passes. Background execution limits, the permission model, and foreground service type declarations all take effect at onceORDER — A workable sequence: check whether an extension applies, plan the Expo SDK upgrade, then run device regression tests on a minimal build. Doing all three at once hides the cause
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 Native233Expo192Android47

As an indie developer, I run ambient sound apps for sleep. 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 removed 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 states that on Android, if you do not enable lock screen controls, background playback stops after approximately three minutes — an OS limitation, not a bug to be fixed on your side.

My build stopping quietly 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 own the lock screen at a time. 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.

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 $15 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-06-26
Keep Audio Playing in the Background and Add Lock Screen Controls in a Rork App
How to make a Rork-generated Expo app keep playing music or healing sounds in the background and expose lock screen and Control Center controls, with working expo-audio code and the platform-specific gotchas.
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-05-04
Microphone and Audio Recording Not Working in Rork — A Symptom-Based Troubleshooting Guide
When microphone or audio recording stops working in a Rork-generated app, the root cause is often not obvious. This guide walks through common failure patterns — permissions, audio mode setup, simulator limits, and async pitfalls — with working code fixes.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →