[object Object]

← back to Exo

fix(macos-app): disable URL response caching for cluster-state polling (#2005)

b26268dfaff0149ef674d547ae83d0874fe42c37 · 2026-05-01 06:41:10 -0400 · ecohash-co

Fixes #2004.

`ClusterStateService` polls `/state` at 2 Hz via `URLSession.shared`,
which keeps an on-disk `URLCache` attached by default. Every polled
response body gets persisted under `~/Library/Caches/exolabs.EXO/`,
sustaining ~500–620 KB/sec of file-backed memory dirtied — far above
macOS's ~25 KB/sec per-process daily-average baseline. Six
microstackshot reports observed on a single Mac Studio M3 Ultra over
eight days, with one 15-hour run accumulating 34.36 GB of cache writes.

Heaviest stack on every diagnostic report (96–98% of samples):

```
_dispatch_workloop_worker_thread → _dispatch_block_async_invoke2 →
  __CFURLCache::CreateAndStoreCacheNode → write
```

Full diagnostic data and analysis in #2004.

## What changed

`ClusterStateService` now defaults to an ephemeral, non-caching
`URLSession` instead of `URLSession.shared`. Cluster-state responses are
time-sensitive and small; nothing benefits from being cached on disk.

```swift
private static func makeNonCachingSession() -> URLSession {
    let config = URLSessionConfiguration.ephemeral
    config.urlCache = nil
    config.requestCachePolicy = .reloadIgnoringLocalCacheData
    return URLSession(configuration: config)
}
```

The existing per-request `request.cachePolicy =
.reloadIgnoringLocalCacheData` calls are kept as defense in depth — they
only affect read behavior, but harmless to leave alongside the
session-level config.

## Scope

- **Behavioral**: none. Polled requests still go out at the same
cadence; responses still parse the same; no semantic change to any API
surface.
- **Test injection**: the `session:` parameter remains in `init`, so
tests can still inject a custom mock session unchanged.
- **`BugReportService` and other `URLSession.shared` callers**:
untouched. If maintainers prefer an app-wide URLCache disable instead,
happy to switch the approach (issue body has the alternative spelled
out).

## Verification

Verified locally that compiling EXO with this change produces a working
menubar app and `ClusterStateService` continues to fetch state
correctly. After ~30 min of idle polling, no new entries in
`/Library/Logs/DiagnosticReports/EXO_*.diag` and no growth in
`~/Library/Caches/exolabs.EXO/`.

## Test plan
- [ ] Build EXO from this branch on macOS 26.4
- [ ] Launch, let cluster state polling run for 30+ min
- [ ] Confirm no new microstackshot diagnostic reports
- [ ] Confirm `~/Library/Caches/exolabs.EXO/Cache.db*` does not grow

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Jordan Miller <jordan.d.miller@gmail.com>

Files touched

Diff

commit b26268dfaff0149ef674d547ae83d0874fe42c37
Author: ecohash-co <team@ecohash.co>
Date:   Fri May 1 06:41:10 2026 -0400

    fix(macos-app): disable URL response caching for cluster-state polling (#2005)
    
    Fixes #2004.
    
    `ClusterStateService` polls `/state` at 2 Hz via `URLSession.shared`,
    which keeps an on-disk `URLCache` attached by default. Every polled
    response body gets persisted under `~/Library/Caches/exolabs.EXO/`,
    sustaining ~500–620 KB/sec of file-backed memory dirtied — far above
    macOS's ~25 KB/sec per-process daily-average baseline. Six
    microstackshot reports observed on a single Mac Studio M3 Ultra over
    eight days, with one 15-hour run accumulating 34.36 GB of cache writes.
    
    Heaviest stack on every diagnostic report (96–98% of samples):
    
    ```
    _dispatch_workloop_worker_thread → _dispatch_block_async_invoke2 →
      __CFURLCache::CreateAndStoreCacheNode → write
    ```
    
    Full diagnostic data and analysis in #2004.
    
    ## What changed
    
    `ClusterStateService` now defaults to an ephemeral, non-caching
    `URLSession` instead of `URLSession.shared`. Cluster-state responses are
    time-sensitive and small; nothing benefits from being cached on disk.
    
    ```swift
    private static func makeNonCachingSession() -> URLSession {
        let config = URLSessionConfiguration.ephemeral
        config.urlCache = nil
        config.requestCachePolicy = .reloadIgnoringLocalCacheData
        return URLSession(configuration: config)
    }
    ```
    
    The existing per-request `request.cachePolicy =
    .reloadIgnoringLocalCacheData` calls are kept as defense in depth — they
    only affect read behavior, but harmless to leave alongside the
    session-level config.
    
    ## Scope
    
    - **Behavioral**: none. Polled requests still go out at the same
    cadence; responses still parse the same; no semantic change to any API
    surface.
    - **Test injection**: the `session:` parameter remains in `init`, so
    tests can still inject a custom mock session unchanged.
    - **`BugReportService` and other `URLSession.shared` callers**:
    untouched. If maintainers prefer an app-wide URLCache disable instead,
    happy to switch the approach (issue body has the alternative spelled
    out).
    
    ## Verification
    
    Verified locally that compiling EXO with this change produces a working
    menubar app and `ClusterStateService` continues to fetch state
    correctly. After ~30 min of idle polling, no new entries in
    `/Library/Logs/DiagnosticReports/EXO_*.diag` and no growth in
    `~/Library/Caches/exolabs.EXO/`.
    
    ## Test plan
    - [ ] Build EXO from this branch on macOS 26.4
    - [ ] Launch, let cluster state polling run for 30+ min
    - [ ] Confirm no new microstackshot diagnostic reports
    - [ ] Confirm `~/Library/Caches/exolabs.EXO/Cache.db*` does not grow
    
    🤖 Generated with [Claude Code](https://claude.com/claude-code)
    
    Co-authored-by: Jordan Miller <jordan.d.miller@gmail.com>
---
 app/EXO/EXO/Services/ClusterStateService.swift | 19 ++++++++++++++++++-
 1 file changed, 18 insertions(+), 1 deletion(-)

diff --git a/app/EXO/EXO/Services/ClusterStateService.swift b/app/EXO/EXO/Services/ClusterStateService.swift
index 27c7ffdf..c4222cde 100644
--- a/app/EXO/EXO/Services/ClusterStateService.swift
+++ b/app/EXO/EXO/Services/ClusterStateService.swift
@@ -17,7 +17,7 @@ final class ClusterStateService: ObservableObject {
 
     init(
         baseURL: URL = URL(string: "http://127.0.0.1:52415")!,
-        session: URLSession = .shared
+        session: URLSession = ClusterStateService.makeNonCachingSession()
     ) {
         self.baseURL = baseURL
         self.endpoint = baseURL.appendingPathComponent("state")
@@ -27,6 +27,23 @@ final class ClusterStateService: ObservableObject {
         self.decoder = decoder
     }
 
+    /// `URLSession.shared` carries an on-disk `URLCache` that persists every
+    /// response body under `~/Library/Caches/exolabs.EXO/`. We poll `/state`
+    /// at 2 Hz from `startPolling`, so leaving the shared cache attached
+    /// dirties ~500–620 KB/sec of file-backed memory and trips macOS's
+    /// per-process `disk writes` resource limit (microstackshot reports
+    /// observed on M3 Ultra producing GBs of cached responses per hour).
+    /// Cluster-state polling responses are time-sensitive and small; they
+    /// gain nothing from being cached on disk. Use an ephemeral session
+    /// with `urlCache = nil` so neither response bodies nor metadata
+    /// touch disk.
+    private static func makeNonCachingSession() -> URLSession {
+        let config = URLSessionConfiguration.ephemeral
+        config.urlCache = nil
+        config.requestCachePolicy = .reloadIgnoringLocalCacheData
+        return URLSession(configuration: config)
+    }
+
     func startPolling(interval: TimeInterval = 0.5) {
         stopPolling()
         Task {

← 8dae3ecb A few targeted tweaks to address HF rate limits (#2009)  ·  back to Exo  ·  feat: update rdma_ctl instructions (#1977) 9c6ff4ce →