The LiveKit Swift SDK supports Remote Procedure Calls (RPC), enabling direct method invocations between participants. RPC provides a type-safe, error-handled way to execute code on remote participants and receive responses.
Overview
RPC enables participants to:
- Call methods on remote participants
- Send and receive structured data
- Handle errors with typed error responses
- Set timeouts for response guarantees
Unlike data messages which are fire-and-forget, RPC provides:
- Request-response semantics
- Built-in timeout handling
- Error propagation
- Acknowledgment tracking
Basic Usage
Registering RPC Methods
Register a method handler to respond to incoming RPC calls:
import LiveKit
try await room.registerRpcMethod("greet") { data in
print("Received greeting from \(data.callerIdentity): \(data.payload)")
return "Hello, \(data.callerIdentity)!"
}
Call a method on a remote participant:
let destinationIdentity = "user-123"
let method = "greet"
let payload = "Hi there!"
do {
let response = try await room.localParticipant.performRpc(
destinationIdentity: destinationIdentity,
method: method,
payload: payload,
responseTimeout: 15
)
print("Received response: \(response)")
} catch let error as RpcError {
print("RPC failed: \(error.message)")
}
RPC Handler
The RpcHandler is a closure that processes incoming RPC requests:
public typealias RpcHandler = @Sendable (RpcInvocationData) async throws -> String
RpcInvocationData
The handler receives RpcInvocationData with:
public struct RpcInvocationData {
/// A unique identifier for this RPC request
public let requestId: String
/// The identity of the RemoteParticipant who initiated the call
public let callerIdentity: Participant.Identity
/// The data sent by the caller (as a string)
public let payload: String
/// The maximum time available to return a response
public let responseTimeout: TimeInterval
}
Handler Example
try await room.registerRpcMethod("calculateSum") { data in
// Parse payload
guard let numbers = try? JSONDecoder().decode(
[Int].self,
from: Data(data.payload.utf8)
) else {
throw RpcError(
code: 2000,
message: "Invalid payload format",
data: ""
)
}
// Perform calculation
let sum = numbers.reduce(0, +)
// Return result
return "\(sum)"
}
Error Handling
RpcError
Throw RpcError to send structured errors back to the caller:
public struct RpcError: Error {
/// The error code (1001-1999 reserved for built-in errors)
public let code: Int
/// A message to include (max 256 bytes, truncated if longer)
public let message: String
/// Optional data payload (max 15KB, truncated if larger)
public let data: String
}
Custom Errors
try await room.registerRpcMethod("divide") { data in
let components = data.payload.components(separatedBy: ",")
guard components.count == 2,
let a = Double(components[0]),
let b = Double(components[1]) else {
throw RpcError(
code: 2001,
message: "Invalid input format",
data: "Expected: 'number,number'"
)
}
guard b != 0 else {
throw RpcError(
code: 2002,
message: "Division by zero",
data: ""
)
}
return "\(a / b)"
}
Built-in Errors
public enum BuiltInError {
case applicationError // 1500: Handler threw non-RpcError
case connectionTimeout // 1501: Connection lost
case responseTimeout // 1502: Handler took too long
case recipientDisconnected // 1503: Recipient left
case responsePayloadTooLarge // 1504: Response exceeds 15KB
case sendFailed // 1505: Network send failed
case unsupportedMethod // 1400: Method not registered
case recipientNotFound // 1401: Destination doesn't exist
case requestPayloadTooLarge // 1402: Request exceeds 15KB
case unsupportedServer // 1403: Server doesn't support RPC
case unsupportedVersion // 1404: Version mismatch
}
Catching Errors
do {
let response = try await room.localParticipant.performRpc(
destinationIdentity: identity,
method: "processData",
payload: jsonPayload
)
handleSuccess(response)
} catch let error as RpcError {
switch error.code {
case 1400: // Unsupported method
print("Method not available on remote participant")
case 1501: // Connection timeout
print("Lost connection to participant")
case 1502: // Response timeout
print("Participant took too long to respond")
default:
print("RPC error (\(error.code)): \(error.message)")
}
} catch {
print("Unexpected error: \(error)")
}
Timeouts
Response Timeout
Set how long to wait for a response:
// Default: 15 seconds
try await room.localParticipant.performRpc(
destinationIdentity: identity,
method: "longRunningTask",
payload: data,
responseTimeout: 30 // Wait up to 30 seconds
)
The minimum effective timeout is 8 seconds to account for round-trip latency buffering. Values below 8 seconds are automatically clamped to 8 seconds.
// From LocalParticipant+RPC.swift:32
// If a value less than 8s is provided, it will be automatically
// clamped to 8s to ensure sufficient time for round-trip latency buffering.
Timeout in Handler
The handler receives the remaining time:
try await room.registerRpcMethod("task") { data in
print("Time available: \(data.responseTimeout)s")
// If not enough time, fail early
guard data.responseTimeout > 10 else {
throw RpcError.builtIn(.responseTimeout)
}
// Perform work
return try await performLongTask()
}
Payload Size Limits
RPC requests and responses are limited to 15KB. Attempting to send larger payloads will result in an error.
// Maximum payload size
let MAX_RPC_PAYLOAD_BYTES = 15360 // 15 KB
// Request too large
try await room.localParticipant.performRpc(
destinationIdentity: identity,
method: "upload",
payload: largeData // > 15KB
)
// Throws: RpcError.builtIn(.requestPayloadTooLarge)
// Response too large
try await room.registerRpcMethod("download") { data in
let response = generateLargeResponse() // > 15KB
return response
}
// Caller receives: RpcError.builtIn(.responsePayloadTooLarge)
For large data, use data channels or storage instead.
Managing Methods
Unregistering Methods
// Unregister a method
await room.unregisterRpcMethod("greet")
Checking Registration
if await room.isRpcMethodRegistered("greet") {
print("Handler is registered")
}
Overwriting Handlers
// First registration
try await room.registerRpcMethod("process") { data in
return "v1"
}
// Attempt to register again
try await room.registerRpcMethod("process") { data in
return "v2"
}
// Throws: LiveKitError - "RPC method 'process' already registered"
// Unregister first, then re-register
await room.unregisterRpcMethod("process")
try await room.registerRpcMethod("process") { data in
return "v2"
}
Advanced Patterns
Request-Response with JSON
import Foundation
struct CalculationRequest: Codable {
let operation: String
let values: [Double]
}
struct CalculationResult: Codable {
let result: Double
}
// Register handler
try await room.registerRpcMethod("calculate") { data in
let request = try JSONDecoder().decode(
CalculationRequest.self,
from: Data(data.payload.utf8)
)
let result: Double
switch request.operation {
case "sum":
result = request.values.reduce(0, +)
case "multiply":
result = request.values.reduce(1, *)
default:
throw RpcError(code: 3000, message: "Unknown operation", data: "")
}
let response = CalculationResult(result: result)
let jsonData = try JSONEncoder().encode(response)
return String(data: jsonData, encoding: .utf8)!
}
// Make call
let request = CalculationRequest(operation: "sum", values: [1, 2, 3, 4, 5])
let requestData = try JSONEncoder().encode(request)
let requestString = String(data: requestData, encoding: .utf8)!
let responseString = try await room.localParticipant.performRpc(
destinationIdentity: identity,
method: "calculate",
payload: requestString
)
let responseData = Data(responseString.utf8)
let result = try JSONDecoder().decode(CalculationResult.self, from: responseData)
print("Result: \(result.result)") // 15
Broadcast RPC
// Call same method on all participants
let participants = room.remoteParticipants.values
await withTaskGroup(of: Void.self) { group in
for participant in participants {
group.addTask {
do {
let response = try await room.localParticipant.performRpc(
destinationIdentity: participant.identity,
method: "ping",
payload: "hello"
)
print("\(participant.identity): \(response)")
} catch {
print("\(participant.identity): failed - \(error)")
}
}
}
}
Rate Limiting
actor RateLimiter {
private var lastCall: Date = .distantPast
private let minimumInterval: TimeInterval
init(minimumInterval: TimeInterval) {
self.minimumInterval = minimumInterval
}
func checkLimit() throws {
let now = Date()
if now.timeIntervalSince(lastCall) < minimumInterval {
throw RpcError(
code: 4000,
message: "Rate limit exceeded",
data: ""
)
}
lastCall = now
}
}
let rateLimiter = RateLimiter(minimumInterval: 1.0)
try await room.registerRpcMethod("limitedMethod") { data in
try await rateLimiter.checkLimit()
return "OK"
}
Objective-C Compatibility
Deprecated wrapper for Objective-C:
// Deprecated: Use async version in Swift instead
[room registerRpcMethod:@"greet"
handler:^NSString *(NSString *callerIdentity, NSString *payload) {
return [NSString stringWithFormat:@"Hello, %@!", callerIdentity];
}
onError:^(NSError *error) {
NSLog(@"Registration failed: %@", error);
}];
The Objective-C wrapper is deprecated. Use the async Swift API instead.
Best Practices
- Keep handlers fast: Respond within the timeout period
- Validate input: Always validate and sanitize payload data
- Use structured data: JSON for complex request/response data
- Handle all errors: Don’t let unhandled errors escape handlers
- Set appropriate timeouts: Balance responsiveness and reliability
- Limit payload size: Use data channels for large data transfers
- Implement retries: For critical operations, retry failed calls
Limitations
- Maximum payload size: 15KB (both request and response)
- Error messages: 256 bytes maximum (truncated if longer)
- Error data: 15KB maximum (truncated if longer)
- Minimum timeout: 8 seconds (includes latency buffer)
- Reserved error codes: 1001-1999 (for built-in errors)
Migration Guide
In previous versions, registerRpcMethod was on LocalParticipant. It has been moved to Room.
// Old (deprecated)
try await room.localParticipant.registerRpcMethod("method") { data in
return "response"
}
// New
try await room.registerRpcMethod("method") { data in
return "response"
}
See Also
- Data Messages - Fire-and-forget messaging
- Room API - Room methods and events
- Source code:
Sources/LiveKit/Core/RPC.swift