Skip to main content
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)!"
}

Performing RPC Calls

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

  1. Keep handlers fast: Respond within the timeout period
  2. Validate input: Always validate and sanitize payload data
  3. Use structured data: JSON for complex request/response data
  4. Handle all errors: Don’t let unhandled errors escape handlers
  5. Set appropriate timeouts: Balance responsiveness and reliability
  6. Limit payload size: Use data channels for large data transfers
  7. 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

Build docs developers (and LLMs) love