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)
swiftlet 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
swift// Preferred: Use the singleton let group = MultiThreadedEventLoopGroup.singleton // Get any EventLoop from the group let eventLoop = group.any()
Bridging EventLoopFuture to async/await
swift// From EventLoopFuture to async let result = try await someFuture.get() // From async to EventLoopFuture let future = eventLoop.makeFutureWithTask { try await someAsyncOperation() }
ByteBuffer Operations
swiftvar 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:
swiftimport 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:
swiftvar 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:
swiftvar 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:
swiftreturn 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


