Swift NIO
Overview
This skill provides expert guidance on SwiftNIO, Apple's event-driven network application framework. Use this skill to help developers write safe, performant networking code, build protocol implementations, and properly integrate with Swift Concurrency.
Agent Behavior Contract (Follow These Rules)
- Analyze the project's
Package.swiftto determine which SwiftNIO packages are used. - Before proposing fixes, identify if Swift Concurrency can be used instead of
EventLoopFuturechains. - Never recommend blocking the EventLoop - this is the most critical rule in SwiftNIO development.
- Prefer
NIOAsyncChanneland structured concurrency over legacyChannelHandlerpatterns for new code. - Use
EventLoopFuture/EventLoopPromiseonly for low-level protocol implementations. - When working with
ByteBuffer, always consider memory ownership and avoid unnecessary copies.
Quick Decision Tree
When a developer needs SwiftNIO guidance, follow this decision tree:
-
Building a TCP/UDP server or client?
- Read
references/Channels.mdfor Channel concepts andNIOAsyncChannel - Use
ServerBootstrapfor TCP servers,DatagramBootstrapfor UDP
- Read
-
Understanding EventLoops?
- Read
references/EventLoops.mdfor event loop concepts - Critical: Never block the EventLoop!
- Read
-
Working with binary data?
- Read
references/ByteBuffer.mdfor buffer operations - Prefer slice views over copies when possible
- Read
-
Implementing a binary protocol?
- Read
references/ByteToMessageCodecs.mdfor codec patterns - Use
ByteToMessageDecoderandMessageToByteEncoder
- Read
-
Migrating from EventLoopFuture to async/await?
- Use
.get()to bridge futures to async - Use
NIOAsyncChannelfor channel-based async code
- Use
Triage-First Playbook (Common Issues -> Solutions)
-
"Blocking the EventLoop"
- Offload CPU-intensive work to a dispatch queue or use
NIOThreadPool - Never perform synchronous I/O on an EventLoop
- See
references/EventLoops.md
- Offload CPU-intensive work to a dispatch queue or use
-
Type mismatch crash in ChannelPipeline
- Ensure
InboundOutof handler N matchesInboundInof handler N+1 - Ensure
OutboundOutof handler N matchesOutboundInof handler N-1 - See
references/Channels.md
- Ensure
-
Implementing binary protocol serialization
- Use
ByteToMessageDecoderfor parsing bytes into messages - Use
MessageToByteEncoderfor serializing messages to bytes - Use
readLengthPrefixedSliceandwriteLengthPrefixedhelpers - See
references/ByteToMessageCodecs.md
- Use
-
Memory issues with ByteBuffer
- Use
readSliceinstead ofreadByteswhen possible - Remember ByteBuffer uses copy-on-write semantics
- See
references/ByteBuffer.md
- Use
-
Deadlock when waiting for EventLoopFuture
- Never
.wait()on a future from within the same EventLoop - Use
.get()from async contexts or chain with.flatMap
- Never
Core Patterns Reference
Creating a TCP Server (Modern Approach)
let server = try await ServerBootstrap(group: MultiThreadedEventLoopGroup.singleton)
.bind(host: "0.0.0.0", port: 8080) { channel in
channel.eventLoop.makeCompletedFuture {
try NIOAsyncChannel(
wrappingChannelSynchronously: channel,
configuration: .init(
inboundType: ByteBuffer.self,
outboundType: ByteBuffer.self
)
)
}
}
try await withThrowingDiscardingTaskGroup { group in
try await server.executeThenClose { clients in
for try await client in clients {
group.addTask {
try await handleClient(client)
}
}
}
}
EventLoopGroup Best Practice
// Preferred: Use the singleton
let group = MultiThreadedEventLoopGroup.singleton
// Get any EventLoop from the group
let eventLoop = group.any()
Bridging EventLoopFuture to async/await
// From EventLoopFuture to async
let result = try await someFuture.get()
// From async to EventLoopFuture
let future = eventLoop.makeFutureWithTask {
try await someAsyncOperation()
}
ByteBuffer Operations
var buffer = ByteBufferAllocator().buffer(capacity: 1024)
// Writing
buffer.writeString("Hello")
buffer.writeInteger(UInt32(42))
// Reading
let string = buffer.readString(length: 5)
let number = buffer.readInteger(as: UInt32.self)
Reference Files
Load these files as needed for specific topics:
EventLoops.md- EventLoop concepts, nonblocking I/O, why blocking is badChannels.md- Channel anatomy, ChannelPipeline, ChannelHandlers, NIOAsyncChannelByteToMessageCodecs.md- ByteToMessageDecoder, MessageToByteEncoder for binary protocol (de)serializationpatterns.md- Advanced integration patterns: ServerChildChannel abstraction, state machines, noncopyable ResponseWriter, graceful shutdown, ByteBuffer patterns
Best Practices Summary
- Never block the EventLoop - Offload heavy work to thread pools
- Use structured concurrency - Prefer
NIOAsyncChannelover legacy handlers - Use the singleton EventLoopGroup -
MultiThreadedEventLoopGroup.singleton - Handle errors in task groups - Throwing from a client task closes the server
- Mind the types in pipelines - Type mismatches crash at runtime
- Use ByteBuffer efficiently - Prefer slices over copies
Use ByteBuffer for Binary Protocol Handling
When parsing or serializing binary data (especially for network protocols), use SwiftNIO's ByteBuffer instead of Foundation's Data. ByteBuffer provides:
- Efficient read/write operations with built-in endianness handling
- Zero-copy slicing with reader/writer index tracking
- Integration with NIO ecosystem
When converting between ByteBuffer and Data, use NIOFoundationCompat:
import NIOFoundationCompat
// ByteBuffer to Data
let data = Data(buffer: byteBuffer)
// Data to ByteBuffer - use writeData for better performance
var buffer = ByteBuffer()
buffer.writeData(data) // Faster than writeBytes(data)
Bad - Using Data with manual byte manipulation:
var buffer = Data()
var messageLength: UInt32?
for try await message in inbound {
buffer.append(message)
if messageLength == nil && buffer.count >= 4 {
messageLength = UInt32(buffer[0]) << 24
| UInt32(buffer[1]) << 16
| UInt32(buffer[2]) << 8
| UInt32(buffer[3])
buffer = Data(buffer.dropFirst(4)) // Copies data!
}
}
Good - Using ByteBuffer:
var buffer = ByteBuffer()
for try await message in inbound {
buffer.writeBytes(message)
if buffer.readableBytes >= 4 {
let readerIndex = buffer.readerIndex
guard let messageLength = buffer.readInteger(endianness: .big, as: UInt32.self) else {
continue
}
if buffer.readableBytes >= messageLength {
guard let bytes = buffer.readBytes(length: Int(messageLength)) else { continue }
// Process bytes...
} else {
// Not enough data yet, reset reader index
buffer.moveReaderIndex(to: readerIndex)
}
}
}
Binary Data Types Comparison
There are several "bag of bytes" data structures in Swift:
| Type | Source | Platform | Notes |
|---|---|---|---|
Array<UInt8> | stdlib | All | Safe, growable, good for Embedded Swift |
InlineArray<N, UInt8> | stdlib (6.1+) | All | Fixed-size, stack-allocated, no heap allocation |
Data | Foundation | All (large binary) | Not always contiguous on Apple platforms |
ByteBuffer | SwiftNIO | All (requires NIO) | Best for network protocols, not Embedded |
Span<UInt8> | stdlib (6.2+) | All | Zero-copy view, requires Swift 6.2+ |
UnsafeBufferPointer<UInt8> | stdlib | All | Unsafe, manual memory management |
Recommendations:
- For iOS/macOS-only projects:
Datais fine due to framework integration - For SwiftNIO-based projects:
ByteBufferis required for I/O operations - For Embedded Swift:
[UInt8]andInlineArray - For cross-platform APIs:
Span<UInt8>(Swift 6.2+) allows any backing type
NIO Channel Pattern with executeThenClose
Use executeThenClose to get inbound/outbound streams from NIOAsyncChannel:
return try await channel.executeThenClose { inbound, outbound in
let socket = Client(inbound: inbound, outbound: outbound, channel: channel.channel)
return try await perform(client)
}
Public API with Internal NIO Types
When exposing async sequences that wrap NIO types:
- Create a custom
AsyncSequencewrapper struct with internal NIO stream - The wrapper's
AsyncIteratortransforms NIO types to public types - This avoids exposing internal NIO imports in public API
SwiftNIO UDP Notes
- Use
DatagramBootstrapfor UDP sockets - Messages use
AddressedEnvelope<ByteBuffer>containing remote address and data - Multicast requires casting channel to
MulticastChannelprotocol - Socket options use
SocketOptionValue(Int32) type so_reuseportis only available on Linux