What does @concurrent do?
@concurrent is a Swift 6.2 attribute that does the opposite of what you might guess — it forces a function to run on the background/global executor rather than the caller’s actor.
The context you need first: Swift 6.2 flipped the default
Before 6.2, a nonisolated async function always hopped to the global executor (background thread) when called, regardless of the caller’s context. That was a constant source of surprise — you’d call a plain async function from @MainActor code and suddenly be off the main actor with Sendable errors everywhere.
Swift 6.2, via SE-0461, changes how async functions pick a thread by default — an unmarked nonisolated async function now inherits the caller’s actor by default, so an await from a @MainActor view model keeps running on the main thread unless you say otherwise. This pairs with SE-0466, which lets you make @MainActor the default isolation for a whole module — the “approachable concurrency” push.
So what does @concurrent actually do?
Marking a function @concurrent means it always runs on the global executor (background thread) — it’s your explicit opt-out from the new “stay on the caller’s actor” default:
@concurrent
func decode<T: Decodable>(_ data: Data) async throws -> T {
let decoder = JSONDecoder()
// runs on a background thread, regardless of caller
return try decoder.decode(T.self, from: data)
}
When you have genuinely CPU-intensive work — decoding a large JSON blob, processing an image, crunching numbers — you mark the function @concurrent, telling Swift “run this off the caller’s actor.”
A few important rules:
- @concurrent cannot be combined with @MainActor or a custom global actor — it only applies to
nonisolatedfunctions, since the whole point is escaping actor isolation. - @concurrent and nonisolated(nonsending) are the two opposing isolation modes for a nonisolated async function: a function either runs where its caller is, or deliberately runs away from it.
- The design philosophy is deliberately conservative: the smallest unit of work possible is made @concurrent to avoid introducing loads of concurrency where it isn’t needed, since concurrency comes with a real complexity cost.
Mental model comparison:
| Pre-6.2 default | 6.2+ default (nonisolated(nonsending)) | @concurrent | |
|---|---|---|---|
nonisolated async func runs on | global executor (background) | caller’s actor | global executor (background), always |
So in effect, @concurrent is the new spelling of “what nonisolated async used to mean automatically” — except now it’s opt-in and explicit, rather than a silent default you had to work around.
One practical note: this all requires enabling the Swift 6.2 upcoming feature flag (SWIFT_APPROACHABLE_CONCURRENCY / AsyncCallerExecution, or setting defaultIsolation in your package manifest) — it’s not automatically retroactive to existing 6.0/6.1 code without opting in.