
If you are chasing intermittent crashes caused by shared mutable state, the direct path is: enable Thread Sanitizer (TSan) through your test scheme or the -enableThreadSanitizer YES flag, run the suite on a hosted Mac, and filter the race blocks out of the log. For arm64-only reproductions or heavy parallel builds, use Figment's M-series Mac starting at $95/month (exact chip, RAM and storage are agreed in the configuration quote). For Swift test suites you can still run on an x86_64 Simulator runtime, the virtual Intel macOS environment at $29/month (2 vCPU, 8 GiB RAM, 80 GiB disk) is enough; pricing and plan inclusions are listed at Figment Mac hosting. Apple documents the enable flags and the simulator-only scope for iOS on the Thread Sanitizer section of the Xcode diagnostics guide, and the upstream instrumentation lives in the Clang ThreadSanitizer documentation.
TSan instruments your binary at compile time, then watches every memory access at runtime and reports the exact pair of conflicting accesses whenever two threads touch the same location without synchronisation.
TSan works at the compiler level. When you enable it, Xcode passes -fsanitize=thread to Clang and -sanitize=thread to the Swift compiler. Every load and store in the resulting binary is wrapped with shadow-memory bookkeeping that tracks which thread last wrote each byte.
The runtime cost is real. Apple's diagnostics guide reports a 2x–20x slowdown and a 5x–10x memory increase for TSan builds. The upstream Clang docs give a narrower slowdown range of 5x–15x. Expect somewhere in that envelope; a suite that normally finishes in 40 seconds can take several minutes under instrumentation.
Apple restricts TSan to 64-bit macOS binaries and to iOS, iPadOS, tvOS, visionOS, and watchOS apps running in Simulator—not on physical devices. That restriction is why a remote Mac is a good fit: it absorbs the overhead while you keep working locally, and the Simulator is where TSan actually produces reports.
Because TSan slows every memory operation, keep the target focused. Run only the modules or test classes that exercise the shared state you suspect.
Xcode GUI (scheme editor): Open your scheme via Product → Scheme → Edit Scheme, select the Diagnostics section under the Test action, and check Thread Sanitizer. The setting is stored in the .xcscheme file, so it travels with the repo.
Command line (CI-friendly): Pass -enableThreadSanitizer YES to xcodebuild:
xcodebuild test \
-project MyApp.xcodeproj \
-scheme MyApp \
-destination 'platform=iOS Simulator,name=iPhone 16,OS=latest' \
-enableThreadSanitizer YES \
2>&1 | tee tsan-raw.log
The tee step keeps the full log for the awk filter below. Substitute your project's real scheme name—the example uses a placeholder, not a scheme you own.
Both Figment tiers can run this invocation. The $29 virtual Intel macOS environment runs a compatible macOS and Xcode toolchain; per Apple's porting-to-Apple-silicon guidance, Xcode 27 is Apple-silicon only, so use Xcode 26 or earlier on Intel and pair it with a Simulator runtime that still ships x86_64 variants (check the exact Xcode/Simulator row in the Xcode release notes). Choose an M-series plan from $95/month when your race only reproduces under arm64 or when Intel build time is the bottleneck.
Access to the $29 VM is through its browser console; inbound SSH is not currently implemented on the VM. Paste the command in the VM's desktop terminal and wrap the run in a tmux or screen session so the test suite keeps running after you close the browser tab. For a physical M-series plan, confirm the remote-access method (SSH, VNC, browser console) in the quote.
TSan can only report races it actually witnesses at runtime. A test that never runs two threads against the same memory produces no output. The most reliable way to force contention is to turn on parallel test workers:
xcodebuild test \
-project MyApp.xcodeproj \
-scheme MyApp \
-destination 'platform=iOS Simulator,name=iPhone 16,OS=latest' \
-enableThreadSanitizer YES \
-parallel-testing-enabled YES \
-maximum-parallel-testing-workers 4 \
2>&1 | tee tsan-raw.log
If you have an async function that writes a property from a background task, add a test that calls it from multiple Task {} blocks and awaits them with withTaskGroup. That pattern reliably surfaces races in actor-less reference types.
A race report in the full log is buried among build output and test pass/fail lines. The artifact for this page is a short awk filter that pulls only the race blocks from tsan-raw.log. Confirm the block terminator against a known TSan log for your toolchain before you rely on it in CI—some runtimes end the block with a ==<pid>== footer, others simply emit a blank line:
awk '
/WARNING: ThreadSanitizer: data race/ { in_race=1 }
in_race { print }
in_race && NF==0 { in_race=0; print "---" }
' tsan-raw.log > tsan-races.log
Sanity check: grep -c "WARNING: ThreadSanitizer: data race" tsan-raw.log should match the number of --- separators in tsan-races.log. If it does not, adjust the terminator pattern (for example, match /^==[0-9]+==\s*$/) before trusting the filtered file.
Each extracted block has this shape:
WARNING: ThreadSanitizer: data race (pid=12345)
Write of size 8 at 0x... by thread T2:
#0 MyModel.update() MyModel.swift:47
#1 ...
Previous read of size 8 at 0x... by main thread:
#0 MyModel.value.getter MyModel.swift:22
...
The two stacks name the contested property and both call sites. Fix the race by protecting that property with an actor, a lock, or a @MainActor annotation—whichever matches the access pattern in your code.
Not every TSan report is a real bug. Two patterns to double-check in Swift:
DispatchQueue.async with a captured immutable value. If the capture is a value type or a class with no writable state, TSan can still flag the access path. Verify by checking whether the flagged property is ever written after initialisation.@MainActor type tested off the main runloop. If the test harness invokes a @MainActor method from a background thread, TSan will report on the wrong thread rather than on a real race. Annotate the test function with @MainActor, or drive it from a live main-runloop test host.Suppress a confirmed false positive with a .tsan-suppressions file:
race:MyModel.frozenConstant
Load it via TSAN_OPTIONS=suppressions=.tsan-suppressions. Use suppressions sparingly—they hide reports without changing the access pattern.
Xcode 16 exposes the SWIFT_STRICT_CONCURRENCY build setting with values minimal, targeted, and complete. Setting it to complete turns actor-isolation violations into compile-time errors rather than runtime races. A useful pairing:
SWIFT_STRICT_CONCURRENCY=complete so the compiler rejects every isolation violation it can prove statically.-enableThreadSanitizer YES to catch the residue—races involving Objective-C types, unstructured Task launches, or legacy GCD code.Compiler errors are free to fix, so you spend TSan's 2x–20x overhead only on dynamic behaviour the type system cannot verify.
A hosted Mac keeps instrumented test runs off your laptop. Start with the $29 virtual Intel macOS environment if your target still has an x86_64 Simulator runtime; move to an M-series plan from $95/month for arm64-only reproductions or heavier builds, with exact hardware agreed in the quote. Request a configuration at Figment or get started directly.
Related guides: