NetReplication
A production-ready network replication system for Roblox that enables secure, efficient server-to-client state synchronization with built-in integrity verification and delta compression. Heavily inspired by loleris's Replica
Delta Compression
Only send what changed to minimize bandwidth usage
Data Integrity
xxHash64 verification detects corruption and tampering
Security First
Client-side writes are blocked and violations are logged
Automatic Recovery
State refresh and retry mechanisms handle network issues
Why NetReplication?
NetReplication provides enterprise-grade features for state management:
- Reliability: Automatic retries, state refresh, and gap detection
- Security: Read-only clients, session keys, and violation tracking
- Performance: Batched updates, delta compression, and configurable replication rates
- Developer Experience: Comprehensive hooks, simulation mode, and observability
- Battle-Tested: Handles packet loss, corruption, desyncs, and malicious clients
Getting Started
Installation
Copy the NetReplication module into your game's ReplicatedStorage.
Basic Server Usage
local NetReplication = require(game.ReplicatedStorage.NetReplication)
-- Initialize on the server
NetReplication.Initialize({
ObservabilityEnabled = true,
})
-- Create a replica
local gameStateReplica = NetReplication.NewReplica({
InitialState = {
RoundNumber = 0,
TimeLeft = 60,
Players = {},
},
ReplicationRate = 20, -- 20 updates per second
})
-- Update the state
gameStateReplica:Set({"RoundNumber"}, 1)
gameStateReplica:Set({"TimeLeft"}, 55)
-- Clean up when done
gameStateReplica:Destroy()
Basic Client Usage
local NetReplication = require(game.ReplicatedStorage.NetReplication)
-- Subscribe to a replica (token obtained from server)
local replica = NetReplication.Subscribe(token, {
Hooks = {
OnChange = function(path, oldValue, newValue)
print("State changed:", path, oldValue, "->", newValue)
end,
},
})
if replica then
-- Read the state
local roundNumber = replica:Get({"RoundNumber"})
print("Current round:", roundNumber)
else
warn("Failed to subscribe to replica")
end
Complete Example: Game State Replication
-- Server Script
local NetReplication = require(game.ReplicatedStorage.NetReplication)
local ReplicatedStorage = game:GetService("ReplicatedStorage")
NetReplication.Initialize()
local gameState = NetReplication.NewReplica({
InitialState = {
Status = "Waiting",
Round = 0,
Players = {},
TimeLeft = 0,
},
ReplicationRate = 10,
UseDeltaCompression = true,
})
-- Share the token with clients
local tokenValue = Instance.new("StringValue")
tokenValue.Name = "GameStateToken"
tokenValue.Value = gameState.Token
tokenValue.Parent = ReplicatedStorage
-- Update game state
while true do
task.wait(1)
local timeLeft = gameState:Get({"TimeLeft"})
gameState:Set({"TimeLeft"}, math.max(0, timeLeft - 1))
end
-- Client Script
local NetReplication = require(game.ReplicatedStorage.NetReplication)
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local tokenValue = ReplicatedStorage:WaitForChild("GameStateToken")
local token = tokenValue.Value
local gameState = NetReplication.Subscribe(token, {
Hooks = {
OnChange = function(path, oldValue, newValue)
if path[1] == "TimeLeft" then
print("Time left:", newValue)
elseif path[1] == "Status" then
print("Game status:", newValue)
end
end,
},
})
if gameState then
print("Connected to game state!")
print("Current status:", gameState:Get({"Status"}))
end
Core Concepts
Replicas
A replica is a server-authoritative data container that automatically replicates to subscribed clients. The server creates replicas and updates them, while clients subscribe to receive updates.
Key Properties:
- Token: Unique identifier for the replica
- Data: The current state (nested tables supported)
- Version: Monotonically increasing version number
- Subscribers: Clients currently receiving updates
Replication Flow
NetReplication uses a batched replication model:
- Server Updates: Call
Replica:Set()to queue changes - Batching: Changes accumulate until the next replication tick
- Transmission: Batch is sent to all subscribers with version and hash
- Client Application: Clients verify integrity and apply changes
- Hooks: OnChange callbacks fire for updated values
Delta Compression
When enabled, NetReplication sends only the changed values instead of the entire state:
- Full State Mode: Sends complete data snapshot every update
- Delta Mode: Sends only path-value pairs that changed
- Threshold: Automatically switches based on data size
local replica = NetReplication.NewReplica({
InitialState = { ... },
UseDeltaCompression = true,
DeltaCompressionThreshold = 10240, -- 10KB threshold
})
Data Integrity
Every update includes an xxHash64 checksum of the complete state. Clients verify this hash to detect:
- Network corruption
- Packet manipulation
- Desynchronization
When integrity checks fail, the client automatically requests a state refresh from the server.
-- Integrity is automatic, but you can check desync status
if replica:IsDesynced() then
warn("Replica is permanently desynced - consider recreating")
end
API Reference
NetReplication.Initialize
NetReplication.Initialize(config: InitConfig?)
Initializes the NetReplication system on server or client. Must be called before using other functions.
Parameters
| Parameter | Type | Description |
|---|---|---|
| config | InitConfig? | Optional configuration table |
Config Options
| Field | Type | Default | Description |
|---|---|---|---|
| SimulationMode | boolean | false | Enable network simulation for testing |
| SimulationConfig | SimulationConfig | nil | Latency, packet loss, corruption settings |
| ObservabilityEnabled | boolean | false | Enable detailed event logging |
Example
NetReplication.Initialize({
ObservabilityEnabled = true,
SimulationMode = game:GetService("RunService"):IsStudio(),
SimulationConfig = {
LatencyMs = 100,
PacketLoss = 0.05,
CorruptionRate = 0.01,
},
})
NetReplication.NewReplica (Server Only)
NetReplication.NewReplica(config: ReplicaConfig): Replica
Creates a new replica on the server.
Parameters
| Parameter | Type | Description |
|---|---|---|
| config | ReplicaConfig | Configuration for the replica |
Config Options
| Field | Type | Default | Description |
|---|---|---|---|
| InitialState | table | required | Starting state of the replica |
| ReplicationRate | number | 10 | Updates per second (max 20 by default) |
| UseDeltaCompression | boolean | false | Send only changed values |
| DeltaCompressionThreshold | number | 10240 | Minimum bytes to use delta mode |
| Hooks | table | nil | Event callbacks |
Returns
A new Replica object with Token, Data, and methods.
Example
local replica = NetReplication.NewReplica({
InitialState = {
Health = 100,
Position = Vector3.new(0, 10, 0),
Inventory = {},
},
ReplicationRate = 15,
UseDeltaCompression = true,
Hooks = {
OnChange = function(path, oldValue, newValue)
print("Changed:", table.concat(path, "."), oldValue, newValue)
end,
OnDestruction = function(replica)
print("Replica destroyed:", replica.Token)
end,
},
})
NetReplication.Subscribe (Client Only)
NetReplication.Subscribe(token: string, config: ReplicaConfig?): Replica?
Subscribes to a server replica. Yields until subscription completes or times out.
Parameters
| Parameter | Type | Description |
|---|---|---|
| token | string | Replica token from server |
| config | ReplicaConfig? | Optional config (mainly for hooks) |
Returns
A client Replica object, or nil if subscription failed.
Example
local replica = NetReplication.Subscribe(token, {
Hooks = {
OnChange = function(path, oldValue, newValue)
-- Update UI
end,
OnDestruction = function()
warn("Server destroyed replica")
end,
},
})
if not replica then
warn("Failed to subscribe")
end
Replica:Set (Server Only)
Replica:Set(path: {string}, value: any)
Updates a value in the replica. Changes are queued and sent in the next batch.
Parameters
| Parameter | Type | Description |
|---|---|---|
| path | {string} | Array of keys to nested value |
| value | any | New value to set |
Example
-- Set top-level value
replica:Set({"Health"}, 75)
-- Set nested value
replica:Set({"Player", "Stats", "Level"}, 5)
-- Set array element
replica:Set({"Inventory", 1}, "Sword")
Replica:Get
Replica:Get(path: {string}): any
Retrieves a value from the replica. Works on both server and client.
Parameters
| Parameter | Type | Description |
|---|---|---|
| path | {string} | Array of keys to nested value |
Returns
The value at the path, or nil if not found.
Example
local health = replica:Get({"Health"})
local level = replica:Get({"Player", "Stats", "Level"})
local firstItem = replica:Get({"Inventory", 1})
Replica:Destroy (Server Only)
Replica:Destroy()
Destroys the replica and notifies all subscribers. Cannot be undone.
Example
-- Clean up when no longer needed
replica:Destroy()
Other Methods
Replica:GetSubscriberCount (Server Only)
Replica:GetSubscriberCount(): number
Returns the number of clients currently subscribed.
Replica:IsDesynced (Client Only)
Replica:IsDesynced(): boolean
Returns true if the replica has permanently desynced and retry limit was exceeded.
Configuration
Replica Configuration
ReplicationRate
Controls how many times per second updates are sent to clients.
local replica = NetReplication.NewReplica({
InitialState = { ... },
ReplicationRate = 20, -- 20 updates per second
})
-- Lower rate = less bandwidth, higher latency
-- Higher rate = more bandwidth, lower latency
-- Max rate depends on BATCH_LOOP_INTERVAL (default: 20hz)
Delta Compression
Reduces bandwidth by sending only changed values instead of the full state.
local replica = NetReplication.NewReplica({
InitialState = { ... },
UseDeltaCompression = true,
DeltaCompressionThreshold = 5120, -- Only use delta if state > 5KB
})
When to use delta compression:
- Large state objects with infrequent changes
- Many subscribers receiving the same updates
- Bandwidth-constrained environments
When to avoid delta compression:
- Small state objects (overhead outweighs benefit)
- Frequent full-state changes
- Very high replication rates
Hooks
Hooks allow you to respond to replication events on both server and client.
OnChange (Server & Client)
Hooks = {
OnChange = function(path, oldValue, newValue)
print("Path:", table.concat(path, "."))
print("Old:", oldValue, "New:", newValue)
end,
}
Called whenever a value changes. On the client, this fires after successful integrity verification. An empty path {} indicates a full state replacement.
OnDestruction (Server & Client)
Hooks = {
OnDestruction = function(replica)
print("Replica destroyed:", replica.Token)
-- Clean up UI, stop listening, etc.
end,
}
OnSecurityViolation (Server Only)
Hooks = {
OnSecurityViolation = function(clientId, violationType, details)
warn("Security violation from client:", clientId)
warn("Type:", violationType)
warn("Details:", details)
-- Log to analytics, ban player, etc.
end,
}
Called when a client attempts to modify the replica or when integrity checks fail. After 5 violations in 60 seconds, the client is automatically kicked.
Simulation Mode
Simulation mode allows testing network conditions without a live server.
NetReplication.Initialize({
SimulationMode = true,
SimulationConfig = {
LatencyMs = 150, -- Add 150ms delay
PacketLoss = 0.1, -- Drop 10% of packets
CorruptionRate = 0.05, -- Corrupt 5% of packets
},
})
Use cases:
- Testing recovery mechanisms
- Simulating poor network conditions
- Validating integrity checks
- Stress testing state refresh
Advanced Features
Security & Validation
NetReplication implements multiple security layers:
Read-Only Clients
Clients cannot call Set() on replicas. Attempts are blocked and reported to the server:
-- This will fail on the client and trigger a security event
replica:Set({"Health"}, 9999)
Session Keys
Each subscription receives a unique session key to prevent replay attacks and unauthorized access.
Integrity Verification
Every batch includes an xxHash64 checksum. Clients verify the hash after applying changes. Mismatches trigger automatic state refresh.
Rate Limiting
Clients are rate-limited on subscription and refresh requests to prevent abuse:
- Subscribe: Max 10 attempts per second
- Refresh: Max 5 attempts per 2 seconds
Violation Tracking
The server tracks security violations per client. After 5 violations in 60 seconds, the client is kicked:
Hooks = {
OnSecurityViolation = function(clientId, violationType, details)
-- violationType can be:
-- "client_attempted_set" - client tried to modify replica
-- "hash_mismatch" - integrity check failed
-- other custom violation types
if violationType == "client_attempted_set" then
-- Log exploit attempt
end
end,
}
Batch Replication
NetReplication batches multiple Set() calls into a single network message:
- Server updates accumulate in a pending queue
- Every replication tick, changes are coalesced by path
- Latest value for each path is sent (older values are dropped)
- Single RemoteEvent fires to all subscribers
-- These three calls are batched into one network message
replica:Set({"Player", "Health"}, 90)
replica:Set({"Player", "Mana"}, 50)
replica:Set({"Player", "Health"}, 85) -- Overwrites first Health update
-- Client receives one batch with:
-- {"Player", "Health"} = 85 (latest)
-- {"Player", "Mana"} = 50
Failed Batch Retry
If a batch fails to send, it's automatically retried up to 3 times with the same data.
State Refresh
When clients detect issues, they can request a full state refresh from the server:
Automatic Triggers
- Integrity failure: Hash mismatch after applying changes
- Sequence gap: Received version is too far ahead (gap > 5)
- Repeated corruption: 3+ integrity failures
- Repeated gaps: 5+ sequence gaps
Refresh Process
- Client sends RefreshState request to server
- Server responds with complete current state, version, and hash
- Client verifies hash and replaces local state
- OnChange hook fires with empty path to signal full replacement
-- Check if client is permanently desynced
if replica:IsDesynced() then
warn("Replica failed to resync after max retries")
-- Consider destroying and resubscribing
end
Exponential Backoff
Failed refresh attempts retry with exponential backoff up to 5 times. After that, the replica is marked as permanently desynced.
Observability
Enable observability to log detailed replication events:
NetReplication.Initialize({
ObservabilityEnabled = true,
})
Logged Events
- ReplicaCreated: New replica created with token
- ReplicaDestroyed: Replica destroyed with subscriber count
- ClientSubscribed: Client subscribed with version
- BatchReplicated: Batch sent with change count and delta mode
- BatchApplied: Client applied batch successfully
- StateRefreshed: State refresh completed
- SecurityViolation: Client triggered security event
- PlayerKicked: Client kicked for repeated violations
All events are printed as JSON with timestamp, event name, and details.
Best Practices
Choose Appropriate Replication Rates
Match the replication rate to your use case:
- High-frequency (15-20hz): Fast-paced gameplay, player movement
- Medium-frequency (5-10hz): Game state, round timers, leaderboards
- Low-frequency (1-2hz): Lobby state, configuration, announcements
Use Delta Compression Wisely
Enable delta compression for large state objects where most of the data stays constant. Avoid it for small states or when everything changes frequently.
Structure Your State Efficiently
-- Good: Flat structure for frequently updated values
{
RoundNumber = 1,
TimeLeft = 60,
Status = "Active",
}
-- Bad: Deep nesting for simple values
{
Game = {
Round = {
Number = 1,
Timer = { TimeLeft = 60 },
},
},
}
Batch Related Changes
-- Good: Multiple changes in one tick get batched
replica:Set({"Player", "Health"}, 100)
replica:Set({"Player", "Mana"}, 100)
replica:Set({"Player", "Level"}, 5)
-- Bad: Waiting between changes wastes updates
replica:Set({"Player", "Health"}, 100)
task.wait(0.1)
replica:Set({"Player", "Mana"}, 100) -- Separate batch
Handle Subscription Failures
local replica = NetReplication.Subscribe(token, config)
if not replica then
warn("Failed to subscribe - falling back to local mode")
-- Create local fallback state
-- or retry after delay
return
end
-- Check for desync during gameplay
if replica:IsDesynced() then
warn("Replica desynced - resubscribing")
replica = NetReplication.Subscribe(token, config)
end
Clean Up Properly
-- Server: Destroy replicas when no longer needed
game:BindToClose(function()
for _, replica in pairs(activeReplicas) do
replica:Destroy()
end
end)
-- Client: Handle destruction gracefully
Hooks = {
OnDestruction = function()
-- Clear UI
-- Stop listening
-- Remove references
end,
}
Use Hooks for Side Effects
-- Good: Use hooks to update UI
Hooks = {
OnChange = function(path, oldValue, newValue)
if path[1] == "TimeLeft" then
updateTimerUI(newValue)
end
end,
}
-- Bad: Polling the state
while true do
local timeLeft = replica:Get({"TimeLeft"})
updateTimerUI(timeLeft)
task.wait(0.1)
end
Monitor Security Violations
Hooks = {
OnSecurityViolation = function(clientId, violationType, details)
-- Log to analytics
logSecurityEvent(clientId, violationType, details)
-- Take action for severe violations
if violationType == "client_attempted_set" then
-- Client is trying to exploit
-- Consider immediate ban
end
end,
}
Test with Simulation Mode
-- Enable in Studio to test recovery
if game:GetService("RunService"):IsStudio() then
NetReplication.Initialize({
SimulationMode = true,
SimulationConfig = {
LatencyMs = 200,
PacketLoss = 0.1,
CorruptionRate = 0.05,
},
})
end
Examples
Example 1: Round-Based Game State
-- Server
local NetReplication = require(game.ReplicatedStorage.NetReplication)
local ReplicatedStorage = game:GetService("ReplicatedStorage")
NetReplication.Initialize()
local gameState = NetReplication.NewReplica({
InitialState = {
Status = "Waiting",
Round = 0,
Players = {},
TimeLeft = 0,
Winner = nil,
},
ReplicationRate = 5,
Hooks = {
OnChange = function(path, oldValue, newValue)
if path[1] == "Status" then
print("Game status changed to:", newValue)
end
end,
},
})
-- Share token with clients
local token = Instance.new("StringValue")
token.Name = "GameStateToken"
token.Value = gameState.Token
token.Parent = ReplicatedStorage
-- Game loop
local function startRound()
gameState:Set({"Status"}, "Active")
gameState:Set({"Round"}, gameState:Get({"Round"}) + 1)
gameState:Set({"TimeLeft"}, 120)
while gameState:Get({"TimeLeft"}) > 0 do
task.wait(1)
gameState:Set({"TimeLeft"}, gameState:Get({"TimeLeft"}) - 1)
end
gameState:Set({"Status"}, "Ended")
gameState:Set({"Winner"}, determineWinner())
task.wait(10)
end
while true do
startRound()
end
-- Client
local NetReplication = require(game.ReplicatedStorage.NetReplication)
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")
local token = ReplicatedStorage:WaitForChild("GameStateToken").Value
local gameState = NetReplication.Subscribe(token, {
Hooks = {
OnChange = function(path, oldValue, newValue)
if path[1] == "Status" then
if newValue == "Active" then
showGameUI()
elseif newValue == "Ended" then
showResultsUI()
end
elseif path[1] == "TimeLeft" then
updateTimer(newValue)
elseif path[1] == "Winner" then
displayWinner(newValue)
end
end,
},
})
if gameState then
print("Connected to game state")
print("Current round:", gameState:Get({"Round"}))
print("Status:", gameState:Get({"Status"}))
end
Example 2: Player Leaderboard
-- Server
local leaderboard = NetReplication.NewReplica({
InitialState = {
Players = {},
LastUpdate = os.time(),
},
ReplicationRate = 2, -- Update twice per second
UseDeltaCompression = true,
})
-- Update player score
local function updateScore(userId, score)
leaderboard:Set({"Players", tostring(userId)}, {
UserId = userId,
Score = score,
Timestamp = os.time(),
})
leaderboard:Set({"LastUpdate"}, os.time())
end
-- Add new player
game.Players.PlayerAdded:Connect(function(player)
updateScore(player.UserId, 0)
end)
-- Remove player
game.Players.PlayerRemoving:Connect(function(player)
leaderboard:Set({"Players", tostring(player.UserId)}, nil)
end)
Example 3: Character Health Replication
-- Server
local characterReplicas = {}
game.Players.PlayerAdded:Connect(function(player)
local replica = NetReplication.NewReplica({
InitialState = {
UserId = player.UserId,
Health = 100,
MaxHealth = 100,
Position = Vector3.new(0, 10, 0),
State = "Idle",
},
ReplicationRate = 15, -- 15hz for smooth updates
UseDeltaCompression = true,
})
characterReplicas[player.UserId] = replica
-- Share token with that specific player
local token = Instance.new("StringValue")
token.Name = "CharacterToken"
token.Value = replica.Token
token.Parent = player
player.CharacterAdded:Connect(function(character)
local humanoid = character:WaitForChild("Humanoid")
humanoid.HealthChanged:Connect(function(health)
replica:Set({"Health"}, health)
end)
humanoid.Died:Connect(function()
replica:Set({"State"}, "Dead")
end)
-- Update position periodically
while humanoid.Health > 0 do
task.wait(0.1)
if character.PrimaryPart then
replica:Set({"Position"}, character.PrimaryPart.Position)
end
end
end)
end)
game.Players.PlayerRemoving:Connect(function(player)
local replica = characterReplicas[player.UserId]
if replica then
replica:Destroy()
characterReplicas[player.UserId] = nil
end
end)
-- Client
local Players = game:GetService("Players")
local player = Players.LocalPlayer
local token = player:WaitForChild("CharacterToken").Value
local characterState = NetReplication.Subscribe(token, {
Hooks = {
OnChange = function(path, oldValue, newValue)
if path[1] == "Health" then
updateHealthBar(newValue)
elseif path[1] == "State" then
if newValue == "Dead" then
showDeathScreen()
end
end
end,
},
})
if characterState then
print("Character state synced")
print("Health:", characterState:Get({"Health"}))
end
Example 4: Global Configuration
-- Server
local config = NetReplication.NewReplica({
InitialState = {
DailyReward = 100,
DoubleXPEnabled = false,
MaintenanceMode = false,
MOTD = "Welcome to the game!",
EventActive = false,
},
ReplicationRate = 1, -- Only 1hz for config
})
-- Admin command to update config
local function setDoubleXP(enabled)
config:Set({"DoubleXPEnabled"}, enabled)
print("Double XP:", enabled)
end
-- All clients automatically receive the update
Troubleshooting
Subscription Fails
Symptom: Subscribe() returns nil
Possible Causes:
- Invalid or expired token
- Server replica was destroyed
- Network remotes not set up correctly
- Timeout (5 second default)
Solutions:
-- Wait for token to exist
local tokenValue = ReplicatedStorage:WaitForChild("GameStateToken", 10)
if not tokenValue then
warn("Token not found")
return
end
-- Verify token is valid
local token = tokenValue.Value
if token == "" then
warn("Empty token")
return
end
-- Try subscribing
local replica = NetReplication.Subscribe(token)
if not replica then
warn("Failed to subscribe - retrying")
task.wait(2)
replica = NetReplication.Subscribe(token)
end
Client Desyncs
Symptom: Client state doesn't match server
Causes:
- Packet loss (real or simulated)
- Corruption (real or simulated)
- State refresh failed
Solutions:
-- Check if permanently desynced
if replica:IsDesynced() then
warn("Replica permanently desynced - resubscribing")
replica = NetReplication.Subscribe(token)
end
-- Monitor with hooks
Hooks = {
OnSecurityViolation = function(clientId, violationType, details)
if violationType == "hash_mismatch" then
warn("Integrity failure detected")
-- State refresh will be automatic
end
end,
}
High Bandwidth Usage
Symptom: Network usage is too high
Solutions:
- Lower the replication rate
- Enable delta compression
- Reduce state size
- Batch more changes together
-- Before: High bandwidth
local replica = NetReplication.NewReplica({
InitialState = { ... },
ReplicationRate = 20, -- Too high for this data
UseDeltaCompression = false,
})
-- After: Optimized
local replica = NetReplication.NewReplica({
InitialState = { ... },
ReplicationRate = 5, -- Lower rate
UseDeltaCompression = true, -- Enable delta
DeltaCompressionThreshold = 5120,
})
Stale Batches Warning
Symptom: "Stale update ignored" warnings in output
Cause: Client received an older version than current
This is usually harmless: The client ignores old updates automatically. However, frequent stale batches may indicate network issues.
Circuit Breaker Open (Future Feature)
Note: While circuit breaker hooks exist in the code, they're not fully implemented. This is a placeholder for future resilience features.
Security Violations
Symptom: OnSecurityViolation hook firing frequently
If violationType is "client_attempted_set":
- A client is trying to modify the replica
- Likely an exploit attempt
- Client will be kicked after 5 violations in 60 seconds
Hooks = {
OnSecurityViolation = function(clientId, violationType, details)
if violationType == "client_attempted_set" then
-- Log to analytics
logExploitAttempt(clientId, details)
-- Consider immediate ban
local player = game.Players:GetPlayerByUserId(clientId)
if player then
player:Kick("Unauthorized modification attempt")
end
end
end,
}
Getting Help
- Enable observability to see detailed logs
- Check all hooks are firing as expected
- Test with simulation mode to isolate issues
- Review the source code for implementation details
- Contact @rituals._ on Discord for support