A pure Swift implementation of the SWIM protocol (Scalable Weakly-consistent
Infection-style Process Group Membership) — membership management and failure
detection for large-scale distributed systems. Embedded-first: the gossip codec and
the value-type membership state machine are Foundation-free, and the byte currency
is [UInt8].
Release status. The released
1.2.0ships the prior API. The Embedded-first API documented here lives on the unreleasedembeddedbranch (M8 pending) and is not tagged — pin to the branch to use it.
- Failure detection — efficient node-failure detection via the ping / ping-req / ack protocol.
- Gossip dissemination — infection-style gossip for membership-update propagation.
- Consistency — incarnation numbers (saturating) for state-precedence management.
- Hardened trust boundary — refutation safety,
maxIncarnationDelta/maxMemberCountsanity bounds, and an optional message authenticator; rejections are typed, never silent. - Embedded-first — the
SWIMWirecodec + value-typeMembershipStatehave no Foundation /any; typed throws; zero-copy parsing.
- Swift 6.2+
- macOS 15+, iOS 18+, tvOS 18+, watchOS 11+, visionOS 2+
Add swift-SWIM to your Package.swift. While the Embedded API is on the unreleased
embedded branch, depend on the branch:
dependencies: [
.package(url: "https://github.com/1amageek/swift-SWIM.git", branch: "embedded")
]Then add the product(s) you need to your target dependencies:
.target(
name: "YourTarget",
dependencies: ["SWIM"] // and/or "SWIMWire" (codec) / "SWIMTransportUDP" (UDP)
)import SWIM
// 1. Create a transport (implement SWIMTransport, or use SWIMUDPTransport)
let transport = MyUDPTransport(localAddress: "192.168.1.1:8000")
// 2. Create the SWIM cluster
let localMember = Member(id: MemberID(id: "node1", address: "192.168.1.1:8000"))
let swim = SWIMCluster(
localMember: localMember,
config: .default,
transport: transport
)
// 3. Start and join the cluster
await swim.start()
try await swim.join(seeds: [seedMemberID])
// 4. Monitor membership changes
for await event in swim.events {
switch event {
case .memberJoined(let member): print("Joined: \(member)")
case .memberSuspected(let member): print("Suspected: \(member)")
case .memberFailed(let member): print("Failed: \(member)")
case .memberRecovered(let member): print("Recovered: \(member)")
case .memberLeft(let id): print("Left: \(id)")
default: break
}
}This package ships three products following the Embedded-first 3-tier API design.
| Product | Tier | Import | Use it for |
|---|---|---|---|
SWIM |
Tier-1 facade | import SWIM |
Run a cluster: SWIMCluster, SWIMTransport, events, config. |
SWIMWire |
Tier-3 codec | import SWIMWire |
The Embedded-clean gossip codec + value-type MembershipState. Not pulled in by import SWIM. |
SWIMTransportUDP |
UDP transport | import SWIMTransportUDP |
SWIMUDPTransport, a ready-made transport built on swift-nio-udp. |
Three layers. The Tier-1 SWIM facade owns synchronization, the clock, and
randomness; the Tier-3 SWIMWire codec + value-type MembershipState own no host
coupling and so compile under Embedded Swift.
┌─────────────────────────────────────────────────────────────┐
│ Tier-1 facade (import SWIM) │
│ SWIMCluster (actor) — ping / ping-req / ack orchestration │
│ MemberList (Mutex<MembershipState> + ContinuousClock) │
│ Disseminator (Mutex<DisseminationState>) │
│ SuspicionTimer (actor), SWIMConfiguration, SWIMEvent │
│ SWIMTransport (protocol), SWIMMessageAuthenticator │
├─────────────────────────────────────────────────────────────┤
│ SWIMTransportUDP (import SWIMTransportUDP) │
│ SWIMUDPTransport — built on NIOUDPTransport (swift-nio-udp)│
├─────────────────────────────────────────────────────────────┤
│ Tier-3 codec (import SWIMWire) │
│ MembershipState (caller-locked value-type state machine) │
│ Member / MemberID / MemberStatus / Incarnation │
│ SWIMMessage / GossipPayload / SWIMMessageCodec │
│ DisseminationState / BroadcastQueue, WriteBuffer/ReadBuffer │
│ - Embedded-clean: no Foundation, no `any`, typed throws │
└─────────────────────────────────────────────────────────────┘
The failure-detection logic lives in SWIMCluster (ping / ping-req / ack
orchestration) over the value-type MembershipState; there is no separate
FailureDetector type. import SWIM re-exports only the curated value/identity
types (Member / MemberID / MemberStatus / Incarnation); a protocol
implementer imports SWIMWire for the codec. See Sources/SWIM/CONTEXT.md for the
load-bearing invariants.
┌─────────────────────────────────────────────────────────────┐
│ Protocol Period │
├─────────────────────────────────────────────────────────────┤
│ 1. Select random member M │
│ 2. Send PING to M │
│ 3. Receive ACK → M is alive │
│ 4. No ACK within timeout: │
│ - Select k random members │
│ - Send PING-REQ(M) to each │
│ - Any ACK received → M is alive │
│ - No ACK → mark M as SUSPECT │
│ 5. Piggyback membership updates on all messages │
└─────────────────────────────────────────────────────────────┘
┌─────────┐
│ ALIVE │◄────────────────────────────┐
└────┬────┘ │
│ no ack ack or │
│ refute │
▼ │
┌─────────┐ │
│ SUSPECT │─────────────────────────────┤
└────┬────┘ │
│ timeout │
▼ │
┌─────────┐ │
│ DEAD │─────────────────────────────┘
└─────────┘ rejoin
Implement SWIMTransport to integrate the network layer, or use the ready-made
SWIMUDPTransport (from the SWIMTransportUDP product, built on swift-nio-udp):
public protocol SWIMTransport: Sendable {
func send(_ message: SWIMMessage, to member: MemberID) async throws
var incomingMessages: AsyncStream<(SWIMMessage, MemberID)> { get }
var localAddress: String { get }
}SWIMMessageCodec.encode (and encodeToBytes) throw a typed SWIMCodecError
(e.g. .stringTooLong) instead of trapping, so an over-long identifier or address
cannot crash the encoder.
Message header: Type (1B) | SeqNum (8B)
Message types: 0x01 Ping (payload) | 0x02 PingRequest (target + payload)
0x03 Ack (target + payload) | 0x04 Nack (target)
GossipPayload: Count (2B) | Updates[]
each update: MemberID len (2B) + ID | Address len (2B) + Address
| Status (1B) | Incarnation (8B)
var config = SWIMConfiguration()
config.protocolPeriod = .milliseconds(200) // time between probes
config.pingTimeout = .milliseconds(100) // ping response timeout
config.indirectProbeCount = 3 // number of indirect probes
config.suspicionMultiplier = 5.0 // suspicion timeout multiplier
config.maxPayloadSize = 10 // max updates per message
config.baseDisseminationLimit = 3 // base count (actual = base * log(N))
config.maxIncarnationDelta = 16 // reject implausible incarnation jumps
config.maxMemberCount = 10_000 // cap the member table
// config.authenticator = MyAuthenticator() // optional message authenticationSWIM is, by default, an unauthenticated gossip protocol: any peer that can reach the cluster can forge membership updates (e.g. claim a higher incarnation to mark a member dead, or to make itself undetectable). swift-SWIM adds these defenses:
- Refutation safety — a refuted or recovered member is never erroneously marked dead. The suspicion-kill path requires the exact incarnation captured when suspicion started, and every recovery route (direct ack, gossiped recovery, self-refutation) cancels the running suspicion timer so it can never fire a stale kill.
- Saturating incarnations — incarnation numbers saturate at
UInt64.maxinstead of wrapping, so a logical clock can never roll back and let stale state out-rank newer state. Saturation is observable viaIncarnation.isSaturated. - Heuristic sanity bounds —
maxIncarnationDeltarejects implausibly large incarnation jumps andmaxMemberCountcaps the member table. The cap is enforced on both admission paths: gossiped updates and ping-sender admission (an unauthenticated ping sender is admitted through the sameapplyGossiptrust boundary, not the trustingupdatepath). Rejections are surfaced as typedMemberListRejectionerrors, never silently dropped. - Optional message authentication — conform to
SWIMMessageAuthenticatorand inject it viaSWIMConfiguration.authenticator. When set, outgoing messages are signed and incoming messages verified before their gossip is trusted; unverifiable datagrams are rejected.
struct MyAuthenticator: SWIMMessageAuthenticator {
func sign(message: SWIMMessage) throws -> [UInt8] { /* compute MAC */ }
func verify(message: SWIMMessage) -> Bool { /* check MAC */ }
}
var config = SWIMConfiguration()
config.authenticator = MyAuthenticator()Residual limitation. Without an authenticator, SWIM trusts unauthenticated wire data. The
maxIncarnationDeltaandmaxMemberCountbounds are heuristic sanity limits, not authentication. For real protection against forged gossip, configure anauthenticator.
The SWIMWire codec is optimized for throughput with minimal allocations: zero-copy
parsing via UnsafeRawBufferPointer and a non-copyable ReadBuffer, @inlinable
encode/decode methods, and reserveCapacity to avoid reallocations.
Measured on Apple Silicon (M-series):
| Operation | Throughput | Latency |
|---|---|---|
| Ping decode (empty) | 8.73M ops/sec | 115 ns |
| Ping encode (empty) | 1.68M ops/sec | 595 ns |
| Decode from bytes | 8.6M ops/sec | 116 ns |
| Encode to bytes | 6.3M ops/sec | 160 ns |
| Ping decode (5 updates) | 658K ops/sec | 1.5 μs |
| Ping encode (5 updates) | 309K ops/sec | 3.2 μs |
| Round-trip (3 updates) | 301K ops/sec | 3.3 μs |
| MemberList update | 1.40M ops/sec | 716 ns |
| Random member selection (3/100) | 324K ops/sec | 3.1 μs |
| Disseminator enqueue+get | 84K ops/sec | 11.9 μs |
Run the benchmarks:
swift test --filter BenchmarkThe SWIMTests and SWIMTransportUDPTests targets cover the facade, codec, and UDP
transport. The Mock / Loopback test transports drive the cluster without a real
network. Run with a timeout to guard against hangs:
// MockTransport for unit tests
let transport = MockTransport(localAddress: "127.0.0.1:8000")
transport.receive(message, from: sender) // simulate an inbound message
let sent = transport.getSentMessages() // inspect outbound messages
// LoopbackTransport for integration tests
let transport1 = LoopbackTransport(localAddress: "127.0.0.1:8000")
let transport2 = LoopbackTransport(localAddress: "127.0.0.1:8001")
transport1.connect(to: transport2)
transport2.connect(to: transport1)swift test- SWIM Paper (Cornell)
- Lifeguard (HashiCorp) — SWIM extensions
- memberlist (Go)
MIT License