application(_:didFinishLaunchingWithOptions:) not called on MDM iPads after overnight idle — app resumes without cold start

We are seeing a strange lifecycle issue on multiple MDM-managed iPads where application(_:didFinishLaunchingWithOptions:) is not called after the device is idle overnight.

Even if we terminate the app manually via the app switcher, the next morning the system does not perform a cold launch. Instead, the app resumes directly in:

applicationDidBecomeActive(_:)

This causes all initialization logic that depends on didFinishLaunching to be completely skipped.

This behavior is consistent across four different supervised MDM devices.

Environment

Devices: iPads enrolled in MDM (supervised)

iOS version: 18.3

Xcode: 16.4

macOS: Sequoia 15.7.2

App type: Standard UIKit iOS app

App: Salux Audiometer (App Store app)

Expected Behavior

If the app was terminated manually using the app switcher, the next launch should:

Start a new process

Trigger application(_:didFinishLaunchingWithOptions:)

Follow the normal cold-start lifecycle

Actual Behavior

After leaving the iPad idle overnight (8–12 hours):

The next launch skips didFinishLaunching

The app resumes directly in applicationDidBecomeActive

No new process is started

App behaves as if it had been suspended, even though it was manually terminated

Logs (Relevant Extracts) Day 1 — Normal cold launch [12:06:44.152 PM] PROCESS_STARTED [12:06:44.214 PM] DID_FINISH_LAUNCHING_START launchOptions=[] [12:06:44.448 PM] DID_FINISH_LAUNCHING_END

We then used the app and terminated it via app switcher.

Day 2 — Unexpected resume without cold start [12:57:49.328 PM] APP_DID_BECOME_ACTIVE

No PROCESS_STARTED No didFinishLaunching No cold-start logs

This means the OS resumed the app from a previous state that should not exist.

Reproducible Steps

Use an MDM-enrolled iPad.

Launch the app normally.

Terminate it manually via the multitasking app switcher.

Leave the device idle overnight (8–12 hours).

Launch the app the next morning.

Observe that:

didFinishLaunching does not fire

applicationDidBecomeActive fires directly

Questions for Apple Engineers / Community

Is this expected behavior on MDM-supervised devices in iOS 18?

Are there any known OS-level changes where terminated apps may be revived from disk/memory?

Could MDM restrictions or background restoration policies override app termination?

How can we ensure that our app always performs a clean initialization when launched after a long idle period?

Additional Information

We have full logs from four separate MDM iPads showing identical behavior. Happy to share a minimal reproducible sample if required.

We are seeing a strange lifecycle issue on multiple MDM-managed iPads where application(_:didFinishLaunchingWithOptions:) is not called after the device is idle overnight.

Even if we terminate the app manually via the app switcher, the next morning the system does not perform a cold launch. Instead, the app resumes directly in:

So, my first question here is how sure you are about EXACTLY what's happening. In particular, how do you KNOW didFinishLaunchingWithOptions is not being called?

In particular, how does your logging infrastructure work and how far "back" in time would it track? My concern here is that what's missing here isn't that didFinishLaunchingWithOptions isn't being called, but that the app was actually initialized at some much earlier point in time when the system launched your app into the background and then suspended it, potentially hours earlier than the point you actually opened it.

We have full logs from four separate MDM iPads showing identical behavior. Happy to share a minimal reproducible sample if required.

Do you have sysdiagnose(s) or just your own app logs? If you have a sysdiagnose, the please file a bug and post the bug number back here. In the bug report please make sure you mention exactly which app launch you consider problematic.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

Hi Kevin,

Q - How we know didFinishLaunchingWithOptions is not being called at user-visible launch time

We agree that the most important question here is when the app was actually initialized versus when the user opens it.

To address this, we have implemented explicit lifecycle logging at the very earliest entry points of the app, including:

  • A log at process start
  • Logs at the very beginning and end of application(_:didFinishLaunchingWithOptions:)
  • Logs for all other UIApplication lifecycle callbacks

We log all lifecycle events (including process start and didFinishLaunchingWithOptions) using a dedicated serial queue and persist them directly to disk.

In simplified terms, our logging flow is:

  • Logs are written via a serial logQueue

  • Each log entry is appended to a file in the app’s Documents directory

  • Logs include:

    • ISO timestamps
    • Explicit lifecycle markers
    • The process ID (pid)

This means:

  • Logs are persisted to disk, not memory
  • Logs survive suspension and backgrounding
  • We do not clear or rotate logs on background transitions
  • Each app process appends to the same log file

How far back the logs go

  • Logs accumulate over time and remain available until explicitly uploaded
  • We upload the full log file to our backend (AWS) at specific trigger points
  • Because logs are file-based, they capture events that occurred many hours earlier

This allows us to determine:

  • Whether a new process was created
  • Whether didFinishLaunchingWithOptions was entered for that process
  • Whether a user-visible launch is happening within an already-running (or resurrected) process

In the problematic cases:

  • The pid remains the same
  • There is no PROCESS_STARTED
  • There is no DID_FINISH_LAUNCHING_START
  • The app resumes directly into applicationDidBecomeActive(_:)

This behavior is consistently reproducible on supervised MDM devices and has not been observed on non-MDM devices so far.

Logs include: ... The process ID (PID)

First off, thank you and congratulations on having the foresight to include that. I've long recommended that developers include that in their logging since, from past experience, I've seen FAR too many "weird problems" that were not in fact "weird" once it was clear that the logging was coming from the same or different process.

The PID remains the same

Then this:

Terminate it manually via the multitasking app switcher.

...did not happen. Per standard Unix semantics, process ID's are uniquely assigned at creation. PIDs are reused if/when the process ID space is exhausted and the "wraps" back around; however, they're a 32-bit value (so exhausting the space isn't trivial) and, more importantly, system process creation is "chaotic" enough that getting the same PID doesn't really happen. It's difficult to do INTENTIONALLY[1], so this isn't something that’s going to happen by accident.

[1] There are a class of security attacks that rely on getting the same PID as a previously/recently terminated process, so some amount of work has gone into figuring out how to do this.

Similarly, all of the things "failed" to happen:

There is no PROCESS_STARTED There is no DID_FINISH_LAUNCHING_START The app resumes directly into applicationDidBecomeActive(_:)

...because your process did not in fact "start“; it was already running.

This behavior is consistently reproducible on supervised MDM devices

What is your MDM configuration actually "doing"? Are you locking the device into single app mode? I haven't looked into this in detail, but I wouldn't be surprised if locking the device into single app mode disabled the normal force quit operation. Similarly, the full range of configuration options is large enough that I wouldn't be surprised if one of them disables force termination.

As a side note, keep in mind that our interfaces are not always "trustworthy" once MDM is involved. Quoting myself:

"Across our technology stack, there are often many points where any given "block" could be implemented, with the general trade-off being that lower-level blocks are harder to bypass but also make it more difficult to provide "feedback" as to what's actually happening. There are exceptions, but in general, our MDM "blocks" tend to be implemented at lower levels of the system at points that minimize the "disruption" to the rest of the system. This both makes them harder to bypass and reduces the risk that they'll cause other problems."

...even if that means that some of our UI may show the wrong value or not work as expected. This forum thread has an example of how this can play out.

With all that context, let me return to here:

Even if we terminate the app manually via the app switcher, the next morning the system does not perform a cold launch. Instead, the app resumes directly in:

What are you actually trying to do here? Why is it important that your app cold launch? If you specifically want your app to cold launch then the simplest way to do so is to simply have it call exit(). While that's generally something we generally discourage, as it will look like your app has crashed, exit() is an API and there are situations where it's a very reasonable choice.

Notably, I STRONGLY recommend that developers of apps that are designed to be "locked" into single app mode and run "forever" include some kind of periodic exit call in their design. The problem here is that it's not unusual for an app to include small bugs (notably Mach port leaks) which will eventually cause catastrophic failure IF the app runs "long enough". It's one thing to test and validate that your app works great when run continually for a few days or a week. It's quite another to validate that your app works great after running for a month... or 3 months... or 6 months[1].

[1] This is not hypothetical. I once helped a developer investigate an app hang that ONLY occurred after his app had been in the foreground CONTINUALLY for 6+ months. I eventually determined that, ironically, the diagnostic code they'd added to improve stability by tracking resource usage (primarily memory) was slowly leaking Mach ports, eventually causing their entire app to seize up.[2] They were able to find and fix the issue, but if they'd simply called "exit()" every few weeks, they'd have never had an issue.

[2] This is also my canonical example of why using our Mach APIs is a terrible idea. They're difficult to use correctly, and incorrect usage will cause wildly unpredictable but generally catastrophic failures.

__
Kevin Elliott
DTS Engineering, CoreOS/Hardware

application(_:didFinishLaunchingWithOptions:) not called on MDM iPads after overnight idle — app resumes without cold start
 
 
Q