Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Messages & Channels

All messages and entity actions are routed through typed channels. A channel is a named type that derives Channel and is registered in the Protocol with a ChannelMode and ChannelDirection.

Core API: Not using Bevy? The bare naia-server / naia-client API is identical in concept but uses a direct method-call style instead of Bevy systems. See Core API Overview.


Channel modes

ModeOrderingReliabilityTypical use
UnorderedUnreliableNoneNoneFire-and-forget telemetry
SequencedUnreliableNewest-winsNonePosition updates (drop stale)
UnorderedReliableNoneGuaranteedOne-off notifications
OrderedReliableFIFOGuaranteedChat, game events
TickBufferedPer tickGuaranteedClient input (tick-stamped)
Bidirectional + ReliableFIFOGuaranteedRequest / response pairs

Channel reliability diagram

graph LR
    subgraph Unreliable
        UU[UnorderedUnreliable<br/>drop freely]
        SU[SequencedUnreliable<br/>keep newest]
    end
    subgraph Reliable
        UR[UnorderedReliable<br/>guaranteed, any order]
        OR[OrderedReliable<br/>guaranteed, FIFO]
        TB[TickBuffered<br/>guaranteed, tick-stamped]
    end

Custom channels

#![allow(unused)]
fn main() {
#[derive(Channel)]
pub struct PlayerInputChannel;

// In protocol builder:
.add_channel::<PlayerInputChannel>(
    ChannelDirection::ClientToServer,
    ChannelMode::TickBuffered(Default::default()),
)
}

Sending and receiving messages

Any struct that derives Message can be sent through a message channel:

#![allow(unused)]
fn main() {
#[derive(Message)]
pub struct ChatMessage {
    pub text: String,
    pub sender: u32,
}
}

Register the type in your Protocol builder:

#![allow(unused)]
fn main() {
Protocol::builder()
    .add_message::<ChatMessage>()
    .build()
}

With the Bevy adapter, send and receive using Server / Client SystemParams and MessageReader:

#![allow(unused)]
fn main() {
use bevy::ecs::message::MessageReader;
use naia_bevy_server::{Server, events::ConnectEvent};
use naia_bevy_client::{Client, DefaultClientTag, events::MessageEvents};
use my_game_shared::{ChatMessage, GameChannel};

// Server — send to a specific client:
fn send_welcome(mut server: Server, mut connect_reader: MessageReader<ConnectEvent>) {
    for ConnectEvent(user_key) in connect_reader.read() {
        let msg = ChatMessage { text: "Welcome!".into(), sender: 0 };
        let _ = server.send_message::<GameChannel, _>(user_key, &msg);
    }
}

// Client — receive:
fn receive_chat(mut chat_reader: MessageReader<MessageEvents<DefaultClientTag>>) {
    for events in chat_reader.read() {
        for msg in events.read::<GameChannel, ChatMessage>() {
            println!("Server: {}", msg.text);
        }
    }
}

// Client — send to server:
fn send_input(mut client: Client<DefaultClientTag>) {
    let msg = ChatMessage { text: "Hello!".into(), sender: 42 };
    client.send_message::<GameChannel, _>(&msg);
}
}

Note: Message types do not use Property<> wrappers — that wrapper is only for Replicate components that participate in per-field delta tracking. Message fields are serialized in full each time the message is sent.


TickBuffered channels

TickBuffered stamps every message with the client tick at which the input occurred. The server buffers them and delivers them via receive_tick_buffer_messages(tick) when the server tick matches. This enables tick-accurate input replay and is the foundation of client-side prediction.

#![allow(unused)]
fn main() {
use bevy::ecs::message::MessageReader;
use naia_bevy_server::{Server, events::TickEvent};
use my_game_shared::{PlayerInputChannel, PlayerInput};

// Server tick handler (Bevy system):
fn handle_tick(
    mut server: Server,
    mut tick_reader: MessageReader<TickEvent>,
) {
    for TickEvent(server_tick) in tick_reader.read() {
        let mut messages = server.receive_tick_buffer_messages(server_tick);
        for (user_key, command) in messages.read::<PlayerInputChannel, PlayerInput>() {
            // command arrived at exactly the right simulation step
            let _ = (user_key, command);
        }
    }
}
}

Warning: Commands that arrive after their target tick has already executed are discarded silently. The client’s normal correction handler handles this as an ordinary misprediction — see Client-Side Prediction & Rollback.


Broadcasting to all users

To send the same message to every connected user, iterate over server.user_keys():

#![allow(unused)]
fn main() {
for user_key in server.user_keys() {
    let _ = server.send_message::<GameChannel, _>(&user_key, &msg);
}
}

naia does not ship a single broadcast call — the per-user loop lets you skip disconnected or unauthenticated users and is explicit about which users receive the message.


Backpressure

ReliableSettings::max_queue_depth caps the unacknowledged message queue. send_message returns Err(MessageQueueFull) when the limit is reached. This prevents a slow or disconnected client from causing unbounded memory growth on the server.

#![allow(unused)]
fn main() {
if let Err(e) = server.send_message::<GameChannel, _>(&user_key, &msg) {
    // user disconnected or queue is full — safe to ignore or log
}
}