Apple Screen Time API bugs: a field report
Apple’s Screen Time APIs can report activity correctly for weeks and then fail in ways that still look “armed”. In our real-device testing, newly registered thresholds arrived in impossible bursts, old events ended new sessions, monitor extensions stopped launching, and report-extension writes disappeared without errors.
This is a field report from the Scrollmates repository and device checklist, not a catalogue of Apple-confirmed defects. Each observation is dated and scoped to what we actually saw.
1. A new interval arrived already spent
On 7 September 2026, an iPhone armed a fresh .poolDrain interval. Within 1.2 seconds, iOS delivered all 14 threshold events belonging to that interval, plus 36 events from an interval already considered dead.
The first cumulative minute sat inside our plausibility slack and was debited. The remaining new-generation events were too large to be real. Yet DeviceActivityCenter.monitoredActivities continued to list the interval as armed.
That state is worse than a missing registration: every one-shot threshold has been consumed, but an “is this activity registered?” diagnostic says yes.
Our response was to validate three facts on every callback:
- Does the event name belong to the live registration generation?
- Could this cumulative threshold have elapsed since the interval was armed?
- Has the last accepted cumulative bookmark advanced consistently?
An impossible live-generation callback is discarded and stamps the generation as suspect in the App Group. The main app replaces it on the next foreground, with a cap of two repair attempts per bond day. The monitor extension does not re-register by itself because the app and extension racing a generation counter creates a double-debit risk.
The cause remains unknown. The same phone armed cleanly several other times that day. We detect the impossible state, not a guessed cause.
2. An old threshold ended a new claim
Later on 7 September, a pacing-session threshold fired normally. iOS then re-delivered eventDidReachThreshold 25 times over four minutes. Five new 2-minute claims created during that period were each ended by the next callback — after 13, 2.5, 8, 7 and 16 seconds.
An isActive flag could not help: a claim was active, just not the claim that registered the old event.
We changed the event name from a generic ending to pacingSession_end_<claimKey>, with a millisecond claim identity stored beside the live session. The extension now rejects an event whose key does not match. It also rejects a matching event that arrives before enough wall-clock time has passed for the usage threshold to be possible.
Rejected one-shot events create their own problem: the legitimate threshold may now be gone. The app therefore re-arms once on foreground with only the remaining duration. A second impossible event ends the claim safely rather than risking an indefinitely unshielded session.
Lesson: event type is not event identity. Encode the logical generation or claim in the event name and validate it at delivery.
3. The monitor extension never launched
Across iOS 17 through iOS 26-class devices, we have seen the system continue counting while never invoking the DeviceActivityMonitor extension at a crossed threshold. The visible symptom was simple: the pool stopped moving. Re-registering an activity that still appeared armed was not enough.
On affected hardware, revoking Family Controls access and granting it again restored delivery. Scrollmates now reconciles on foreground and provides an in-app Screen Time access reset flow. The repair re-registers the daily reset, pool drain, schedules and phase shields after the authorisation transition.
This workaround has costs, and one of them is documented rather than observed. Apple’s FamilyActivitySelection page states: “If a user, parent, or guardian revokes authorization of your app, any tokens that [the picker] provided while your app was authorized are voided.” A repair that cycles authorisation is therefore also a repair that discards the user’s stored selection. Plan for re-selection, and distinguish an absent selection from undecodable bytes before deciding what to show.
The other costs are ours: revocation temporarily removes shields, restarts Apple’s cumulative counter and can lose up to the unreported tail between coarse thresholds. A running session or block timer makes the reset unsafe, so the UI refuses it then.
Do not advertise re-granting as a universal fix. It is the repair that worked on our devices for this failure shape.
4. Report-extension App Group writes silently vanished
Our pacing countdown runs in a DeviceActivityReport extension. It needed a baseline so a bond-day aggregate could be converted into “usage since this claim”. We first wrote that baseline to the shared App Group.
The write API returned normally. The value never appeared.
On the owner’s phone, a claim created at 09:18 still had no pacing.session.report.* keys after two hours and many foregrounds. Every fresh report process treated the next render as its first and reset the countdown.
We now treat the report extension as read-only with respect to the App Group and keep its display baseline in the extension’s own UserDefaults.standard, keyed by session identity. The main app still writes the live claim state that the report reads.
Silent storage failure is particularly dangerous because the code looks more trustworthy than an explicit sandbox error. Verify persistence from another process on hardware.
We are not alone in this one. In August 2026 an unrelated developer shipping their first FamilyControls app posted a list of undocumented constraints to r/iOSProgramming, and their first item describes the same behaviour from the opposite direction: “The report extension is a black hole by design… App Group writes silently no-op, there’s no network, no notifications out. If your architecture assumes ‘read usage → store it → use it in the app’, throw that away now.” Two teams reaching the same architecture through the same silent failure is not a confirmed defect, but it is a better warning than either report alone.
5. One documented gotcha that is not a bug
The most expensive failure in this category may not be a defect at all. DeviceActivityEvent thresholds count from the moment you arm them, not from the start of the day — so an app that registers a 30-minute limit at 6pm grants a fresh 30 minutes regardless of what the person has already done.
Apple documents the switch. The includesPastActivity parameter, added in iOS 17.4, controls “whether the system takes into account the person’s device activity before your app starts monitoring the event”, with Apple’s own example: “if your app calls [monitoring] at 1:30pm with a schedule of 1:00pm to 2:00pm, then this boolean determines whether any activity between 1:00 PM and 1:30 PM will contribute to its threshold.”
Without it, a “daily limit” silently means “limit from now”. We are flagging it here rather than claiming it, because we did not discover it — the r/iOSProgramming field note above names it, and their additional warning that the flag applies only to newly registered events, so existing installs need a forced re-arm, is theirs and unverified by us. The parameter, its iOS 17.4 availability and Apple’s wording were checked against Apple’s documentation on 9 September 2026.
6. Real-device logs require their own playbook
The simulator does not run the Screen Time extensions, so the decisive evidence comes from a phone.
Our current capture command is:
idevicesyslog | grep -E "ScrollmatesDA|Scrollmates\["
log stream --device is gone on the current macOS setup we use. Give extension lines a stable prefix (ScrollmatesDA in our case) and include generation, cumulative value, event name and decision in every branch.
For App Group inspection, devicectl device copy from can pull a container snapshot. Two traps cost us time:
- zsh defines
log, so confirm which executable a shell command resolves to; cfprefsdcan overwrite direct plist edits while the app is running, so quit the process before changing a test container.
Seed configuration before first launch when possible. Treat plist surgery as a controlled test input, not proof that production code persisted a value.
7. Make tests prove they ran
Screen Time delivery still needs device rows, but the decision code around it can be pure and heavily tested: stale versus live generation, plausible elapsed time, capped repairs, claim-key matching and storage triage.
When filtering Swift Testing through xcodebuild, do not accept the final success banner as proof that the intended test executed. Record the discovered/executed count and search the result bundle or output for the named test case. Filter at suite level when method-level syntax produces an empty selection in your Xcode version.
We could not re-run that particular command-line trap in the current sandbox because CoreSimulator and package caches were unavailable, so this is a verification rule rather than a fresh reproduction claim.
Frequently asked questions
Are these Apple-confirmed bugs?
No. They are dated observations from Scrollmates device logs and engineering records. Some resemble public developer reports, but we label only what we reproduced.
Why not calculate everything when the app returns?
The shared pool and shields need background delivery. Foreground reconciliation reduces damage but cannot protect the interval while the app remains closed.
Do ApplicationTokens survive reinstall or re-authorisation?
Do not assume they do. Scrollmates treats an absent stored selection after reinstall differently from undecodable bytes, and its reset flow verifies shielding after re-authorisation. We have not promoted community reports of token reissuance into a reproduced claim here.
Can these bugs be tested in the simulator?
Not end to end. Pure policy code can be unit tested, but DeviceActivity delivery and extension lifecycle need a real device.
Publish the uncertainty too
Framework failures become folklore when every team reports only a workaround. A useful field note includes the date, device conditions, exact impossible observation, detection rule, repair limit and what remains unknown. That makes the next engineer’s log easier to interpret — even when the operating system does something new.