---
title: "Emptying Caches When macOS Reports Memory Pressure"
url: https://deallocatedobjects.com/posts/emptying-caches-when-macos-reports-memory-pressure
author: Jake Marsh
published: 2026-11-11
updated: 2026-11-11
topics: ["Apple", "Xcode & Tools"]
description: "NSCache evicts entries on its own when memory gets tight, but a plain Swift dictionary keeps everything until you clear it."
---

# Emptying Caches When macOS Reports Memory Pressure

By Jake Marsh, November 11, 2026. HTML: https://deallocatedobjects.com/posts/emptying-caches-when-macos-reports-memory-pressure

`NSCache` evicts entries on its own when memory gets tight, but a plain Swift dictionary keeps everything until you clear it. [Postage](https://postageformac.com/?ref=deallocated-post), my Mac email client, has several dictionaries for things like normalized subjects and cleaned-up message text, so it listens for memory pressure itself:

```swift title="SharedEmailStore.swift"
let pressure = DispatchSource.makeMemoryPressureSource(eventMask: [.warning, .critical], queue: .main)
pressure.setEventHandler {
    MainActor.assumeIsolated { Self.purgeCaches() }
}
pressure.activate()
memoryPressureSource = pressure
```

Keep that reference in a property. If the source deallocates, your handler stops firing.

The purge itself is one short function:

```swift
static func purgeCaches() {
    ThreadFilter.purgeCaches()
    SubjectInfoCache.purge()
    HTMLMail.purgeCache()
    MessageText.purgeCache()
    URLCache.shared.removeAllCachedResponses()
}
```

### What Makes It Safe

Purging at a random moment only works if nothing depends on an entry being there. Each of these caches computes on a miss, so an empty cache costs some recomputing and returns the same answers. Here's the subject cache:

```swift title="Email.swift"
static func info(for subject: String) -> (normalized: String, isReply: Bool) {
    lock.lock()
    if let hit = entries[subject] { lock.unlock(); return hit }
    lock.unlock()
    let info = (normalized: EmailThread.normalizeSubject(subject).lowercased(),
                isReply: EmailThread.isReplySubject(subject))
    lock.lock()
    if entries.count >= 50_000 { entries.removeAll(keepingCapacity: true) }
    entries[subject] = info
    lock.unlock()
    return info
}
```

The lock matters here because the handler runs on the main queue, while lookups can come from elsewhere.

Anything that can't be rebuilt doesn't belong in this list, like unsaved drafts or state that only lives in memory. Postage has a test, `purgingCachesChangesNoResults`, that checks filtering and threading return the same results after a purge.

### Trying It

macOS ships a tool that puts the system at a given pressure level, so you don't have to fill your RAM to see the handler run:

```bash
sudo memory_pressure -S -l warn
```

`-S` simulates the level, and `-l` takes `warn` or `critical`. The whole system sees it, so other apps will react too. Put a breakpoint in your handler the first time to confirm it fires.
