Monitor and Safely Reclaim Xcode Disk Space on a Cloud Mac

DevOps & CI/CD ·~6 min read

Monitor and Safely Reclaim Xcode Disk Space on a Cloud Mac

A cloud Mac used continuously for archiving and automated testing is often more likely to fail because of disk pressure than faulty code. Xcode continuously writes DerivedData, archives, device support files, and simulator data. When the disk is nearly full, the compiler may report misleading write errors, while the signing stage may leave behind incomplete artifacts. The reliable approach is not to wipe every directory on a schedule, but to measure usage first, enforce a pre-build capacity gate, and then clean up data in layers based on how easily it can be regenerated or recovered.

Establish a Disk Usage Baseline First

Start by recording the system’s available capacity, then inspect the development directories. df shows how much space the file system can still write, while du identifies what is consuming that space. The two commands serve different purposes and are not interchangeable.

df -Pk /

for path in \
  "$HOME/Library/Developer/Xcode/DerivedData" \
  "$HOME/Library/Developer/Xcode/Archives" \
  "$HOME/Library/Developer/CoreSimulator" \
  "$HOME/Library/Developer/Xcode/iOS DeviceSupport"
do
  if [ -e "$path" ]; then
    du -sk "$path"
  fi
done

Take measurements before a clean build, after the archive completes, and again after testing finishes. These three readings reveal how much a single job adds and help distinguish normal usage peaks from an actual storage leak. To identify the largest directories inside DerivedData, run:

du -sk "$HOME/Library/Developer/Xcode/DerivedData"/* 2>/dev/null \
  | sort -nr \
  | head -20

Available space reported on APFS may be affected by purgeable data. A build gate should use the available blocks reported by df; do not assume a job will finish based only on nominal directory sizes.

Define an Explicit Capacity Gate for Builds

Set the capacity threshold from measured peak usage rather than relying on a fixed rule of thumb. Use 30GB as an initial value and record the lowest remaining capacity during a complete archive. After several stable runs, adjust the threshold to 1.5 times the observed peak growth, with additional room for exported artifacts.

Place the following script at the start of the pipeline. If capacity is insufficient, it exits with status code 75, allowing the scheduler to mark the job as temporarily unavailable instead of starting a build that is likely to fail.

#!/bin/zsh
set -euo pipefail

minimum_kb=$((30 * 1024 * 1024))
available_kb=$(df -Pk / | awk 'NR == 2 {print $4}')

if (( available_kb < minimum_kb )); then
  printf 'Insufficient disk capacity: %s KB available
' "$available_kb"
  exit 75
fi

printf 'Disk capacity check passed: %s KB available
' "$available_kb"

Store thresholds separately for each node. Different Xcode versions, simulator combinations, and project sizes produce different peaks, so results from a small project should not be used to validate capacity for a large workspace.

Isolate Each Build in Its Own Directory

A shared default DerivedData directory creates more than a capacity problem: it also makes it difficult to determine which job owns each directory. Assign a dedicated path to every workspace so it can be removed precisely after the job finishes without affecting other builds that are still running.

job_root="$HOME/build-jobs/$BUILD_ID"
derived_data="$job_root/DerivedData"
archive_path="$job_root/artifacts/App.xcarchive"

mkdir -p "$job_root/artifacts"

xcodebuild \
  -workspace App.xcworkspace \
  -scheme App \
  -configuration Release \
  -derivedDataPath "$derived_data" \
  -archivePath "$archive_path" \
  archive

BUILD_ID should come from the job system and be restricted to letters, numbers, periods, underscores, or hyphens. Before deleting anything, also verify that the target path is located under build-jobs, preventing an empty variable from expanding the deletion scope to the home directory.

Clean Up in Layers Based on Recoverability

Begin with data that can be regenerated and leave archives requiring human review until last.

Level Data Recommended action Primary constraint
1 DerivedData from completed jobs Delete by job directory Confirm that no build process is using it
2 Unavailable simulator records Clean up with simctl Do not delete the Devices directory directly
3 Old device support files Review against versions actually used for testing Retain OS versions still needed for debugging
4 xcarchive and dSYM files Move them out after manual review Released versions must remain traceable

Use the system utility to remove unavailable simulators:

xcrun simctl delete unavailable

Do not empty CoreSimulator/Devices directly. If the directory contents no longer match the simulator service state, later device creation and startup failures become much harder to diagnose. Archives should not be deleted solely because they are “older than a certain number of days,” either, because an xcarchive may contain the dSYM associated with a released version. Record the version, build number, and destination first, then remove the local copy.

Avoid Three Common Automated Cleanup Pitfalls

First, never clean shared directories while a build is running. Even if a file appears old, the compiler may still reference it through an index or intermediate artifact. Cleanup jobs should acquire a node-level lock or operate only on isolated work directories already marked as complete.

Second, do not treat “deletion succeeded” as proof that space has been released. Run df -Pk / again after deletion and verify that the number of available blocks has actually increased. If it has not, look for deleted files that are still open by running processes instead of repeatedly issuing the same deletion command.

Third, do not let a script decide whether an archive is important. Unattended jobs should only list cleanup candidates, including details such as directory size, last modification time, and associated build number. Release records should determine whether an item is moved or deleted.

Validate the Result with a Repeatable Build

After cleanup, run at least one archive identical to the production job. Record the starting capacity, lowest capacity, ending capacity, archive path, and exit status, and confirm that the .xcarchive was generated completely. If the node also runs simulator tests, start each retained version in the test matrix once and verify that its device can be created, booted, and shut down normally.

The final checklist can remain short: the pre-build gate passed; the job used isolated DerivedData; no compiler processes remained after exit; the archive and dSYM were recorded; simctl list shows no abnormal devices; and available disk space exceeds the threshold for the next job. Cleanup is complete only when all of these conditions are satisfied.

Frequently asked questions

How much free disk space should an Xcode build node keep?

There is no universal value. Start with a 30GB pre-build minimum, record the peak usage of a complete archive, and reserve at least 1.5 times that peak for large multi-module projects.

Is it safe to delete the entire DerivedData directory?

Yes, but only when no active build is using it. A safer design assigns a separate DerivedDataPath to each workspace and removes only directories belonging to completed jobs.

Which Xcode files should never be removed automatically?

Exclude in-progress archives, unbacked xcarchive bundles, dSYM files tied to released builds, and simulator devices that remain part of an active test matrix.

Dedicated physical node

Keep a reproducible environment on the same cloud Mac

Choose a fixed model, region, and rental term, then use the full macOS graphical interface and command line for development or automation tasks.

Rent a cloud Mac now