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-clientAPI is identical in concept but uses a direct method-call style instead of Bevy systems. See Core API Overview.
Channel modes
| Mode | Ordering | Reliability | Typical use |
|---|---|---|---|
UnorderedUnreliable | None | None | Fire-and-forget telemetry |
SequencedUnreliable | Newest-wins | None | Position updates (drop stale) |
UnorderedReliable | None | Guaranteed | One-off notifications |
OrderedReliable | FIFO | Guaranteed | Chat, game events |
TickBuffered | Per tick | Guaranteed | Client input (tick-stamped) |
| Bidirectional + Reliable | FIFO | Guaranteed | Request / 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:
Messagetypes do not useProperty<>wrappers — that wrapper is only forReplicatecomponents 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
}
}