4 min read

SQLite Databases in App Group Containers: Just Don't

Using SQLite in an App Group container might seem like a simple solution, but it can lead to several frustrating issues and crashes. Here’s why 0xDEAD10CC crashes happen and what to do instead.

It’s a common problem developers face when building iOS apps: you have a SQLite database, possibly backed by Core Data, and you want to use it to support features in your app extensions, such as widgets and intents.

The problem has a common and seemingly simple answer: just move the database to an App Group container! It's relatively straightforward, with plenty of StackOverflow answers and YouTube tutorials on the matter.

But now you have several new problems that aren't immediately obvious:

  • You start to receive crash logs with error code 0xDEAD10CC.
  • You sometimes get SQLITE_BUSY and related crashes when the database is opened at app launch.
  • Background tasks—like HealthKit updates or silent push notifications—become less reliable.

Of these, 0xDEAD10CC is the most frustrating. It’s hard to reproduce, surprisingly difficult to fix, and often misunderstood.

Why 0xDEAD10CC Errors Occur

What a dramatic error code! But it doesn’t indicate a traditional multithreading deadlock. Instead, 0xDEAD10CC is iOS’s way of avoiding a situation where a file—or more often, a SQLite database—remains indefinitely locked because a suspended process never releases it.

SQLite relies on file locks to ensure data integrity by preventing multiple processes or threads from writing to the database at the same time. These locks are critical, but they become problematic on iOS, where apps are aggressively suspended in the background.

If your app holds a lock during a SQLite transaction and then gets suspended, and the database is stored in an App Group container, other processes such as widgets or App Intents may be blocked from ever accessing it. This can lead to instability and ultimately a bad user experience.

To prevent that outcome, iOS forcefully terminates the suspended app with error code 0xDEAD10CC.

The good news? This termination usually happens silently in the background, so users are unlikely to notice. Still, Apple reports these terminations as crashes in App Store Connect and Xcode Organizer.

0xDEAD10CC Mitigations

To mitigate 0xDEAD10CC terminations and other errors that can arise with a SQLite database in an App Group container, you can implement the following mitigations:

  1. Enable WAL mode.
  2. Asynchronously open the database connection.
  3. Use read-only connections whenever possible.
  4. Observe when the app is backgrounded, and ensure that no SQLite file locks are acquired while your app is in the background. Then, update your application logic accordingly.
  5. Use a background task (beginBackgroundTask(expirationHandler:)) to extend execution time and ensure any active SQLite operations have time to complete and release locks before suspension. This approach was recommended by Technical Note TN2151 (now removed from developer.apple.com, but still available via the Internet Archive). The Iconfactory blogged about how this solution was sufficient for them in 2019.

For more information, I recommend reading GRDB's guide to Sharing a Database.

These mitigations may sound reasonable at first, but implementing all of them correctly and consistently is surprisingly difficult—especially for database-heavy apps like Foodnoms. Trust me—I’ve tried.

Beyond the complexity, you also lose flexibility with background execution. For example, features like HealthKit background delivery become harder to support reliably.


EDIT: May 14, 2025

Gwendal Roué (aka @groue), author of GRDB, responded on Mastodon with additional context on why completely avoiding these crashes is so tricky:

One of the difficulties lies in the OS apis, in order to detect when the app is about to get suspended, and when the app becomes able to acquire database locks again. See this forum thread with @justkwin.

The other difficulty is that if the app is writing (holding a lock) at the moment it is notified it will get suspended, then all it can do is aborting the write. Here you can give up and lose data, or plan for writing later 😖

Alternative Solutions for Apps in 2025

Instead of moving your database to a group container, use these techniques instead and keep the database inside your app's documents directory.

Widgets

In your main app process, materialize the minimal state required for your widgets into a Codable struct. Whenever the state changes, serialize this state and write it to a small JSON file in the group container. Use NSFileCoordinator for safe read/write access.

This approach works well for any app extension that only needs a small snapshot of a subset of the user’s app data.

Interactive Widgets

For interactive widgets, write two copies of the intent that both implement ForegroundContinuableIntent. They must have the same name and parameter structure. One copy will be a member of your Widget extension and the other will be a member of the app process. The Widget extension copy can have an empty perform implementation, which will never be used.

/// Widget Extension copy
/// See `WidgetLogFood` in the main app for full implementation
struct WidgetLogFood: AppIntent {
    static var title: LocalizedStringResource = "Log Food"
    static var description = IntentDescription("Log food from a widget.")
    static var isDiscoverable = false

    @Parameter
    var foodID: String

    init() {}

    init(foodID: String) {
        self.foodID = foodID
    }

    func perform() async throws -> some IntentResult {
        return .result()
    }
}

@available(iOSApplicationExtension, unavailable)
extension WidgetLogFood: ForegroundContinuableIntent {}

It may seem like a strange solution, but it works—and it was acknowledged by an Apple App Intents framework engineer.

App Intents

App Intents made several significant improvements over the legacy SiriKit Intents framework. Perhaps the most underrated improvement, however, was that apps are no longer required to execute handling of intents in a separate app extension process. Now, they can be executed as part of the main app process. (You can still use an app extension if you prefer.)

This lifted a huge constraint for apps like Foodnoms, which often writes data when users execute intents. One fewer process that needs the ability to read and mutate app data.

Real World Results

In a recent update, Foodnoms performed a migration to move all SQLite databases from the app group container to the app's documents directory.

Last week was the first full week with this change deployed to all customers. The result is significant: only 47 reported crashes last week—less than half the number before the fix was deployed.

Crashes by week for Foodnoms from Feb 1 to May 7

This is just a chart though. What really matters is the user impact. Well, as explained before, most users are unlikely to notice a difference. However, since the app is not terminated as often in the background, users may notice that the app is doing a better job of retaining its state after being closed and reopened.

What is more exciting is that I am testing re-enabling background delivery for HealthKit data, which could enable more frequent updates to Foodnoms Widgets without requiring the app to be opened.

You may say 47 crashes a week is still a lot, and I agree. After too many years of ignoring them, I am continuing to focus on fixing these rare, but ever-present, crashes. I plan to share more about my learnings in future blog posts.