The macOS Notarization Performance Mystery: When TCC Checks Kill Your Frame Rate

A deep dive into debugging a performance issue that only appeared in notarized macOS apps—and the hidden cost of ScreenCaptureKit security checks.

I maintain Picker, a macOS menu bar color picker utility. It’s a simple app: capture a small 28×28 pixel area around the cursor at 60 FPS using ScreenCaptureKit, display a live preview, and let users copy colors in various formats.

Everything worked perfectly—until I notarized the app for distribution.

The Problem

The symptom was bizarre:

Build TypeResult
Debug build → /ApplicationsSmooth 60 FPS
Archive build (non-notarized) → /ApplicationsSmooth 60 FPS
Archive build (notarized + stapled) → /ApplicationsExtremely laggy

The exact same code, the exact same binary, just with a different code signature and a notarization ticket stapled to it.

The Investigation

Ruling Out the Obvious

First, I verified everything I could think of was identical between builds:

  • Entitlements (both sandboxed)
  • Info.plist
  • Linked libraries (otool -L output identical)
  • Hardened Runtime enabled on both
  • No App Nap (verified in Activity Monitor)
  • Metal shader cache (keyed by bundle ID, not signing identity)

The only differences were:

  1. Signing certificate: Apple Development vs Developer ID Application
  2. Notarization ticket (stapled)

Time Profiler Findings

Instruments showed the heaviest stack trace was in image processing:

NSImage.sampleColor() → 188ms
  └─ -[NSImage TIFFRepresentation] → 74ms
      └─ CGImageMetadataCreateFromLegacyProps → 27ms
          └─ yyparse → 6ms (metadata parser!)

I optimized this—replaced tiffRepresentation with direct CGImage pixel reading, added a cached Metal-backed CIContext. These were good improvements, but they didn’t fix the notarization lag.

The Breakthrough

I was stuck, so I threw the entire codebase at a reasoning model for analysis. The response pointed out something I’d missed:

“Your code calls SCShareableContent.excludingDesktopWindows() on every mouse move. If Developer ID signing adds even a few milliseconds of overhead to TCC checks, your design crosses from ‘just fast enough’ to ‘backlogged’.”

The Smoking Gun: tccd Logging

I monitored the TCC daemon while using the app:

log stream --predicate 'process == "tccd" OR process == "trustd"' --info

Nearly 600 log events in 1 second.

The log showed repeated calls like:

AUTHREQ_CTX: service=kTCCServiceScreenCapture
-[TCCDAccessIdentity staticCode]: static code for: identifier net.domzilla.picker
AUTHREQ_RESULT: authValue=2, authReason=4

Every single call to ScreenCaptureKit APIs was triggering a full TCC permission check, which includes static code signature validation. For Developer ID signed apps, this validation is more expensive—it involves certificate chain validation and potentially revocation checks.

The Root Cause

My original architecture was fundamentally flawed:

func updateCaptureRect(for location: NSPoint) async {
    // Called on every mouse move

    // Problem 1: TCC check on every call!
    let content = try await SCShareableContent.excludingDesktopWindows(...)

    // Problem 2: Another TCC check!
    let config = createStreamConfiguration(for: location)  // 28×28 rect around cursor
    try await stream.updateConfiguration(config)
}

I was capturing a tiny 28×28 pixel rectangle that followed the cursor. Every time the mouse moved, I had to:

  1. Call SCShareableContent to check displays → TCC check
  2. Call stream.updateConfiguration() to move the capture rect → TCC check

At 60+ mouse events per second, each triggering TCC checks that take ~15-20ms for notarized apps, the system was completely overwhelmed.

The architectural mistake: Treating ScreenCaptureKit like a low-level graphics API when it’s actually a security-gated system service.

The Fix

The solution wasn’t caching or throttling—it was rethinking the architecture entirely.

Instead of capturing a small moving rectangle, capture the entire screen and crop in software. The stream configuration gets set once at startup. On each mouse move, I just update a cursor position variable—the stream keeps running unchanged, no ScreenCaptureKit calls needed. The cropping happens in the stream output handler using GPU-accelerated CIImage operations.

For display configuration changes (monitor connect/disconnect), I listen to NSApplication.didChangeScreenParametersNotification instead of polling.

Result: TCC checks reduced from 600/second to essentially zero during normal use.

TCC checks now only occur:

  1. Once when the stream starts
  2. When cursor moves to a different display (multi-monitor setups)
  3. When macOS notifies us that display configuration changed

The preview is now blazing fast in notarized builds—identical to development builds.

Lessons Learned

1. ScreenCaptureKit is a Security API, Not a Graphics API

Don’t treat it like Core Graphics or Metal. Every interaction with ScreenCaptureKit potentially involves the TCC daemon validating your app’s permissions. Design your architecture to minimize these interactions.

2. Notarization Changes Runtime Behavior

Even though notarization is “just” code signing + Apple’s blessing, it changes how macOS treats your app at runtime. Security checks that are fast for development certificates can be significantly slower for Developer ID certificates.

The staticCode evaluation in the TCC logs is the key difference—it validates the full certificate chain for Developer ID apps, while development certificates get a faster path.

3. Test Performance with Notarized Builds

Debug and archive builds aren’t enough. The performance characteristics of your app can be fundamentally different once it’s signed with Developer ID and notarized. Add this to your release testing checklist.

4. When Debugging Performance, Monitor System Daemons

The issue wasn’t in my code—it was in how my code interacted with system services. log stream filtering for tccd, trustd, and syspolicyd revealed what no profiler could.

log stream --predicate 'process == "tccd" OR process == "trustd"' --info

These daemons are the gatekeepers of macOS security. If your app feels slow and you can’t find the bottleneck in your code, check if you’re triggering excessive security checks.

5. Prefer Event-Driven Over Polling

Instead of polling for display changes every N seconds (which triggers TCC checks), listen to NSApplication.didChangeScreenParametersNotification. This is both more efficient and more responsive.

Tools That Helped

  • Instruments (Time Profiler): Initial profiling—showed where time was spent but not why
  • AI Code Analysis: Reasoning models can analyze full codebases and spot architectural issues
  • log stream: System daemon monitoring—the breakthrough tool
  • codesign -dv: Code signature comparison between builds
  • Activity Monitor: Ruled out App Nap

The Hidden Cost of Security

macOS security has become increasingly sophisticated. TCC (Transparency, Consent, and Control), Gatekeeper, notarization, and the Hardened Runtime all work together to protect users. But this security has a cost, and that cost isn’t always visible to developers.

When you call an API that touches protected resources, you’re not just calling that API—you’re invoking a chain of security validations. For development builds, that chain is short. For notarized apps signed with Developer ID certificates, that chain is longer and more thorough.

This is a good thing for security. But as developers, we need to be aware of it and design our apps accordingly.


This was one of the most challenging bugs I’ve debugged. The symptom (laggy preview) seemed like a graphics issue, but the root cause was an architectural mismatch with macOS’s security model.

The fix wasn’t adding caches or throttles—those were band-aids. The real fix was understanding that ScreenCaptureKit’s updateConfiguration() isn’t a lightweight operation. It’s a security boundary crossing. Once I understood that, the solution was obvious: don’t cross the boundary on every mouse move.

If you’re building a macOS app that uses ScreenCaptureKit:

  • Capture more, configure less - Capture a larger area and crop in software
  • Configure once - Set up your stream at start, avoid runtime reconfiguration
  • Use notifications, not polling - React to system events instead of checking periodically
  • Test notarized builds - Performance differs between signing identities

Every ScreenCaptureKit API call might be a security check in disguise. Design accordingly.