Advanced Concurrency

Sendable Protocol Fundamentals

August 20, 2026
5 min read
Featured image for blog post: Sendable Protocol Fundamentals

Sendable is a marker protocol that says "this type is safe to share across concurrency boundaries." Swift 6 enforces this strictly.

Follow along with the code: iOS-Practice on GitHub

Why Sendable Exists

When you pass data to a Task or actor, it crosses a concurrency boundary. If that data is mutable and shared, you get data races.

actor DataStore {
    func save(_ settings: UserSettings) {
        // If UserSettings is a class with mutable properties,
        // another thread could modify it while we're saving!
    }
}

Sendable is the compiler's way of ensuring this can't happen.

What's Automatically Sendable

Value types with Sendable properties:

struct Point: Sendable {
    let x: Double
    let y: Double
}

struct User: Sendable {
    let id: UUID
    let name: String
    let email: String
}

The compiler auto-synthesizes Sendable for structs/enums with all Sendable properties.

Actors:

actor Counter: Sendable {  // Always Sendable
    var count = 0
}

Actors are inherently safe—that's their purpose.

Immutable classes:

final class Config: Sendable {
    let apiKey: String
    let environment: String

    init(apiKey: String, environment: String) {
        self.apiKey = apiKey
        self.environment = environment
    }
}

final + only let properties = Sendable.

What's NOT Sendable

Mutable classes:

class UserSettings {  // NOT Sendable
    var theme: String = "light"
    var fontSize: Int = 14
}

Multiple threads could modify theme simultaneously.

Classes with mutable reference properties:

class Container: Sendable {  // ERROR
    let items: NSMutableArray  // NSMutableArray isn't Sendable
}

@Sendable Closures

Closures that cross concurrency boundaries must be @Sendable:

Task { @Sendable in
    // This closure is Sendable
    await actor.doWork()
}

func process(_ action: @Sendable () async -> Void) async {
    await action()
}

Sendable closures can't capture mutable variables:

var count = 0

Task {
    count += 1  // ERROR: Mutation of captured var in Sendable closure
}

Making Types Sendable

Option 1: Use struct

// Instead of class, use struct
struct UserData: Sendable {
    let id: UUID
    var name: String  // Structs copy, so mutation is safe
}

Option 2: Make class immutable

final class ImmutableUser: Sendable {
    let id: UUID
    let name: String

    init(id: UUID, name: String) {
        self.id = id
        self.name = name
    }
}

Option 3: @unchecked Sendable (you guarantee safety)

final class ThreadSafeCache: @unchecked Sendable {
    private let lock = NSLock()
    private var storage: [String: Data] = [:]

    func get(_ key: String) -> Data? {
        lock.lock()
        defer { lock.unlock() }
        return storage[key]
    }
}

Common Sendable Types

TypeSendable?
Int, Double, BoolYes
StringYes
Array, Dictionary (of Sendable)Yes
Struct (all props Sendable)Yes
Enum (all payloads Sendable)Yes
ActorYes
Class (mutable)No
NSObject subclassesUsually no

Interview Tip

Sendable is foundational to Swift concurrency safety. When discussing actors or async/await, mention: "Data passed across actor boundaries must be Sendable, either value types or carefully designed reference types."

Originally published on pixelper.com

© 2026 Christopher Moore / Dead Pixel Studio

Let's work together

Professional discovery, design, and complete technical coverage for your ideas

Get in touch