Building an online FPS or TPS in Unreal Engine 5 is not a matter of choosing one "networking library." A production game usually has three networking layers:
- Gameplay networking — movement, shooting, damage, ammo, projectiles, doors, objectives, and match state.
- Online services — authentication, friends, invites, lobbies, sessions, matchmaking, voice, P2P, and relay.
- Dedicated-server operations — allocation, region selection, health checks, deployment, logs, scaling, draining, and shutdown.
For most UE shooters, start with built-in Replication, RPCs, and UCharacterMovementComponent. Use a Dedicated Server when fairness matters; consider a Listen Server for small co-op. Add Steam/EOS, GAS, Replication Graph, Iris, or external SDKs only when targets or measurements justify them.
Technical baseline: August 31, 2026. The article uses UE 5.8 documentation and public UE 5.8.1/5.8.2 hotfix notes. The Network Emulation caveat below comes from source inspection of UE 5.8.0 CL
55116800; that engine build was not re-run while preparing this English edition.
Think in three layers, not one library
Gameplay networking
This layer synchronizes gameplay under latency, packet loss, reordering, relevance filtering, and bandwidth limits. UE Replication is a strong default because Actor ownership, per-connection relevancy, priority, dormancy, RPCs, property replication, and Character Movement prediction all live in the same networking model [1-9].
Online services
Steam and EOS answer questions such as "who is the player?", "which lobby are they in?", and "where should they connect?" They do not replace Actor replication after the connection is established. In UE 5.8, Online Services is still documented as Beta, so projects shipping soon should evaluate it against the established Online Subsystem path [17-21].
Server operations
A Dedicated Server still needs to be started, allocated, monitored, updated, drained, and terminated. Amazon GameLift Servers, PlayFab Multiplayer Servers, and Agones help with this operational layer [24-27]. They do not replace Replication.
Competitive FPS: start from a server-authoritative Dedicated Server
Unreal multiplayer is fundamentally client-server, with the server holding the authoritative world state [1,2]. That maps naturally to competitive shooters.
A Listen Server can be perfectly reasonable for a two-to-four-player co-op game, but the host has direct access to the authoritative process. In PvP, that becomes a fairness problem in addition to host departure, NAT, and hardware variance.
A Dedicated Server costs more to operate, but gives you a cleaner authoritative process for validation and consistent simulation. Choose by measured Actors, AI, shot frequency, server frame time, and bandwidth—not player count alone.
Put replicated state in the right gameplay class
A common Dedicated Server migration bug is trying to read server-only objects from client UI.
A useful division is:
-
GameMode: server-only rules such as win conditions, spawn rules, admission rules. -
GameState: public match state such as phase, team score, and round-end server time. -
PlayerState: public per-player data such as team, kills, and deaths. -
PlayerController: input, owner-only UI, and owner-only notifications. -
Pawn/Character: the current body, movement, and weapon interaction.
GameMode does not exist on clients. PlayerController is normally relevant only to its owning client, so it is also the wrong place for a scoreboard that everybody must see [31,32].
For countdowns, replicate a round end server timestamp, not "seconds remaining" every second. Clients can derive the display from AGameStateBase::GetServerWorldTimeSeconds() [31,35], including after late join.
Use UCharacterMovementComponent before inventing movement RPCs
Do not start an FPS by sending your own transform or velocity RPC every frame.
UCharacterMovementComponent already implements local prediction, saved moves, server simulation, correction, and smoothing for remote Characters [5]. Extend movement modes and saved moves for sprinting, sliding, or similar mechanics instead of creating a second synchronization system on top of it.
For unusual vehicles or physics, custom prediction or the Network Prediction plugin may be relevant, but the UE 5.8 API still marks that plugin Beta [12]. Prototype before depending on it.
Separate state from events
The most useful rule in UE networking is simple:
- State that must be reconstructable belongs in Replicated Properties.
- Transient events may use RPCs when appropriate.
HP, ammo, equipped weapon, death state, doors, score, and match phase should survive packet loss, late join, and becoming relevant again. That makes them state.
Muzzle flashes, impact particles, and one-shot sounds may fit Unreliable RPCs, but prefer deriving visuals from replicated state when possible. Reliable RPCs are for low-frequency operations retried while the connection and channel remain valid; overuse can block later reliable traffic [3].
Also remember ownership: a client can normally send a Server RPC only through an Actor it owns. Input RPCs therefore belong on the owning PlayerController, Pawn, or an owned Component/Weapon whose owner chain is correct [3,32].
Do not turn RPC ordering into a game rule
UE documents representative ordering for one Actor under specific send/bunch conditions [33]. Do not generalize that into a gameplay protocol across Actors, Components, frames, loss, or reordering.
ERemoteFunctionSendPolicy::ForceSend also has important limits: it is supported by Replication Graph or Iris, and only sends immediately when invoked in the supported NetDriver::PostTickDispatch / NetWorldTickTime phase. Outside that phase it behaves like Default. ForceQueue is different: it serializes later in the net update if bandwidth remains [3,33].
Use sequence numbers, generations/epochs, and idempotent application logic instead.
Server-authoritative shooting: separate intent from result
The most dangerous shooter implementation is accepting HitActor, bone, or damage values sent by the client.
The client may send intent: which weapon, aim direction, and a timestamp estimate. The server decides the result.
The example below focuses on hitscan weapons. Slow projectiles, predicted projectiles, explosive overlap, and physics interaction need different designs.
Give every shot a lifecycle-aware key
A plain uint16 Sequence is not enough if the weapon respawns, ownership changes, or a sequence wraps. Use a server-assigned source plus an epoch:
USTRUCT()
struct FShotKey
{
GENERATED_BODY()
UPROPERTY() uint32 FireSourceId = 0; // server assigned
UPROPERTY() uint16 FireEpoch = 0; // changes when the sequence space resets
UPROPERTY() uint16 Sequence = 0;
};
USTRUCT()
struct FShotRequest
{
GENERATED_BODY()
UPROPERTY() FShotKey Key;
UPROPERTY() uint8 WeaponSlot = 0;
UPROPERTY() FVector_NetQuantizeNormal AimDirection;
UPROPERTY() uint32 EstimatedServerTimeMs = 0;
};
Before calling the Server RPC, the owning client records (FireSourceId, FireEpoch, Sequence) in a client-process prediction history and plays predicted local FX once. Do not tie that history to the current possession state. Keep it long enough to cover your maximum network/reconciliation delay, and namespace or clear it across disconnect, Server Travel, and session changes.
This matters because a delayed multicast may arrive after death, unpossess, repossess, or controller transfer. Looking only at IsLocallyControlled() at receive time is not enough.
Validate the caller separately from the shot stream
There are two trust questions:
- Which network connection actually sent this Server RPC?
- Is the
FireSourceId/ epoch / slot in the request a stream that the server assigned to that connection?
Do not mix them.
A useful conceptual flow is:
CaptureOriginalCallerContext()
-> ResolveReconciliationComponent(OriginalConnection)
-> ValidateAssignedShotStream(Request, Caller)
-> obtain CanonicalKey
-> validate ammo/rate/time/aim
-> authoritative trace
-> PublishCanonicalShotResolution(...)
Only a canonical, server-validated shot key should enter the authoritative result cache or owner-only reconciliation snapshot.
Treat invalid keys differently from ordinary gameplay rejection:
- Unassigned source, future epoch, or a source belonging to another connection: audit/strike/disconnect policy; do not mutate result state.
- A stale epoch caused by an allowed switching race: optionally return one transient rejection to the original connection, but do not advance the authoritative snapshot.
- Valid key but empty magazine, rate limit, invalid timing window, or excessive aim delta: normal rejection; cache it like an accepted shot so duplicates stay idempotent.
Keep the result route tied to the original connection
Do not resolve the destination again from the Weapon's current owner when the result is ready. Resolve the reconciliation component from the connection captured at RPC dispatch time, ideally on an object that outlives the Pawn such as the original PlayerController.
Conceptually:
UMyShotReconciliationComponent* Reconciliation =
ResolveReconciliationComponent(Caller.OriginalConnection);
if (TryGetCachedResolution(CanonicalKey, Cached))
{
Reconciliation->PublishCanonicalShotResolution(Stream, Cached);
return;
}
if (!ValidateGameplayShot(...))
{
Reconciliation->PublishCanonicalShotResolution(
Stream,
BuildResolution(CanonicalKey, false, false, Ammo, RejectReason));
return;
}
const FServerShotResult Result = RewindAndTraceOnServer(Context);
ConsumeAmmoOnServer(Stream);
ApplyDamageOnServer(Result);
Reconciliation->PublishCanonicalShotResolution(
Stream,
BuildResolution(CanonicalKey, true, Result.bHitConfirmed, Ammo, None));
MulticastPlayConfirmedFireFx(CanonicalKey, Result.CosmeticData);
Duplicates return the cached result to the same original connection without repeating ammo consumption, damage, snapshot mutation, or multicast.
Reconciliation needs an immediate path and a state path
An Unreliable Client RPC is useful for low-latency feedback, but it can be lost. Keep a compact owner-only replicated state as the convergence path.
For example, track:
FireSourceIdFireEpochLastContiguousResolvedSequence- a small recent accepted/rejected/hit detail window
- authoritative ammo
Register it as owner-only replication; the UPROPERTY declaration alone does not do that:
void UMyShotReconciliationComponent::GetLifetimeReplicatedProps(
TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME_CONDITION(
UMyShotReconciliationComponent,
ShotSyncState,
COND_OwnerOnly);
}
If the reconciliation object is a replicated Component/Subobject, also configure its actual replication/registration path [4,16].
Predicted owner FX and confirmed remote FX are different responsibilities
The owner already played the shot immediately. The multicast mainly exists for other clients.
Use a client-process PredictedFxHistory to suppress a confirmed FX key that this local process already predicted, and a separate ConfirmedFxHistory to make remote FX idempotent. Perform that check in an application handler that does not depend on current possession:
void AMyWeapon::ApplyConfirmedFxIfNew(
FShotKey Key,
const FFireFxData& FxData)
{
if (GetClientShotFxRegistry().WasPredictedByAnyLocalPlayer(Key))
{
return;
}
if (!ConfirmedFxHistory.RememberIfNew(Key))
{
return;
}
PlayConfirmedFireFx(FxData);
}
This survives delayed multicast delivery across death, unpossess, repossess, and ownership transfer; non-predicting AI/weapons simply play confirmed FX once.
A useful regression test delays each client's application-handler entry after network delivery but before the prediction-history check, changes possession/ownership, then resumes. The history-based implementation should still produce one total FX per client. A mutation that replaces the history check with IsLocallyControlled() should fail. Delaying only PlayConfirmedFireFx() is too late to catch that bug.
The handlers must be idempotent
Your design should remain correct for representative paths such as:
| Scenario | Owner FX | Result convergence |
|---|---|---|
| Client RPC arrives first | 1 predicted FX | Immediate result; later property is harmless |
| Property arrives first | 1 predicted FX | Snapshot resolves; later RPC is harmless |
| Multicast arrives first | Still 1 predicted FX | Later RPC/property resolves the shot |
| Server RPC request is lost | 1 predicted FX | Later sequence/heartbeat or owner timeout cancels provisional state |
| Client result RPC is lost | 1 predicted FX | Owner-only property converges |
| Multicast is lost | Owner still has predicted FX | Authoritative damage/ammo remain correct |
| Same game handler injected twice | At most one FX | Ammo/damage/result apply once |
Packet duplication does not prove the same gameplay RPC reached application code twice. Test idempotence by directly injecting the same key twice into authority, result, and FX handlers.
Aim validation and lag compensation
Checking that an aim vector is normalized is not enough.
Reconstruct the trace origin from server-known historical view/Pawn/weapon state, not a client position. Compare the submitted direction with the accepted server view using state-specific tolerance for hip fire, ADS, recoil, view switching, turrets, or vehicles; also validate range, spread, interval, ammo, reload, and equipment. This constrains impossible trajectories, not all aimbots.
For lag compensation, keep only needed history. Player hitboxes are common, while doors, shields, lifts, and destructible cover may also require historical collision state. Prefer a non-replicated historical collision proxy over moving live Actors. If real Components are rewound, use a scoped guard that restores transform/collision on every exit path and does not emit extra overlaps, gameplay events, physics wake-ups, or replication dirtiness.
For timestamps, GetServerWorldTimeSeconds() is a useful synchronization entry point, but do not blindly trust a client value. Account for RTT, jitter, outliers, reconnects, Server Travel, and long sessions [35]. A match-relative quantized integer tick or double is safer than an ever-growing absolute float.
Automatic weapons: design failure modes, not only happy paths
There is no single correct fire protocol.
| Model | Benefit | Main failure mode |
|---|---|---|
| Reliable start/stop | Low message count, easy server timer | Stop can wait behind reliable traffic; add state number, max duration, and forced stop on death/weapon change |
| Unreliable input state + heartbeat | Converges on later heartbeat | Use sequence + timeout to discard stale input |
| Unreliable request per shot | Easy per-shot aim/result validation | Lost shot request means that shot never happened; high ROF increases traffic and reconciliation work |
Choose by fire rate, aim-update needs, packet-loss behavior, reliable-queue pressure, and cheat resistance.
GAS, Replication Graph, and Iris solve different problems
GAS
Gameplay Ability System is valuable when abilities, attributes, effects, tags, cooldowns, costs, and prediction need one model [13,14]. A rifle-focused FPS may not need it everywhere; using normal gunplay Components plus GAS for complex abilities can be simpler.
Replication Graph
Replication Graph targets large Actor/connection counts by reusing persistent graph nodes for candidate selection [10]. Add it when relevance selection is measured as a bottleneck, after fixing unnecessary replication, relevancy/cull distance, update frequency, dormancy, priority, and data layout.
Iris
UE 5.8 documentation is awkward here: the 5.8 release notes call Iris production-ready, while UE 5.8 Iris pages still display Experimental warnings. Iris also remains opt-in [11,34]. Treat the exact engine patch, platform, plugins, replicated Subobjects, serializers, RepNotify behavior, Fast Arrays, and owner-only conditions as a regression-test matrix.
And one very important rule: Iris and Replication Graph are not used together on the same NetDriver. Iris does not support Replication Graph; filtering and prioritization take over those responsibilities [37-39]. Compare a Generic Replication + Replication Graph configuration against an Iris configuration rather than "adding Iris on top."
Steam and EOS are the control plane, not gameplay replication
For Steam-only PC games, Online Subsystem Steam can handle login/session/invite/presence, with Steam Sockets where appropriate [19,20]. For multi-store or cross-platform plans, EOS provides authentication, lobbies, sessions, stats, voice, and P2P-related services [17,18]. Keep vendor APIs behind a game-facing abstraction; Lyra's Common User plugin is a useful flow reference [21].
Photon Fusion for Unreal is a different authority model
Photon Fusion for Unreal 3 uses Shared Authority. That can be attractive for some co-op or distributed-authority games, but it is not the same model as one authoritative UE Dedicated Server [22].
Fusion still reuses replicated/RepNotify-style properties and UE Character Networking [22,40], but logic/tooling built around one server ROLE_Authority needs deeper redesign. At the August 31, 2026 baseline, Fusion for Unreal 3.0.0 is Preview; Windows/Android are supported, macOS/iOS are insufficiently tested, and console/Linux support is planned [23].
Use HTTP, WebSocket, and gRPC for backend work
Use HTTP, WebSocket, or gRPC for accounts, inventory, matchmaking, allocation, results, telemetry, and LiveOps—not per-frame Character Movement or shooting. Replacing NetDriver gameplay traffic means rebuilding UE networking behavior. Keep trusted databases/services behind server-side APIs.
Dedicated-server hosting choices
- Amazon GameLift Servers: placement, process integration, health, and scaling [24].
- PlayFab Multiplayer Servers: PlayFab-friendly hosting with an Unreal GSDK in the server build [25].
- Agones: Kubernetes-native GameServer/Fleet/Allocation; its Unreal Client SDK/plugin belongs in the Dedicated Server process [26,27].
Compare regions, startup/warm pools, UDP/ports, rollout/rollback, autoscaling, DDoS boundaries, observability, crash handling, lock-in, and team expertise.
Optimize bandwidth by not sending data
Before compression, reduce who receives what.
- Relevancy — owner relevancy, cull distance, conditional replication [4,6,9].
- Update frequency — tune by Actor role; it is not the server simulation tick.
- Dormancy — wake/flush before changing replicated state [7].
- Priority — relative scheduling under saturation, not total bandwidth [8].
- Representation — quantize data and send IDs/state instead of verbose payloads.
Fast Array Serializer helps with changing collections such as inventory/status effects [15]; do not treat replicated order as UI sort order. For replicated Subobjects, manage registration/removal carefully so replication never touches an invalid reference; Iris adds Subobject/fragment considerations [16,37].
Cheat resistance starts at the Server RPC boundary
Validate ownership, legal state transitions, ammo/rate/speed/distance, authoritative or historical spatial state, RPC frequency, and malformed values such as NaN, invalid enums, oversized arrays, and unknown IDs.
WithValidation failure can disconnect the caller [3]. Here, structural/protocol abuse is separated from ordinary latency/state-race rejection; that is a project policy, not a universal UE rule. Never embed service, admin, or database secrets in the player build.
Treat bad networks as a normal test environment
PIE with two local windows is not a networking test plan.
Also test separate processes, packaged Client/Server builds, different machines/VMs, real online-service accounts, travel/reconnect, and bot load.
Be careful with DropUnreliableRPC in UE 5.8.0
The official command reference documents NetEmulation.DropUnreliableRPC <RPCName> <0-100> [36]. Source inspection of Launcher UE 5.8.0 CL 55116800 found a contradictory percentage/block path, so 100 should not be used as proof that 100% of the target RPC was dropped. The public UE 5.8.1/5.8.2 hotfix lists do not identify a fix for that command [41,42].
Therefore, for deterministic acceptance tests, prefer a test-build-only project hook that explicitly skips the target call.
For example, a server-side test CVar can skip only ClientResolveShot while still updating the owner-only snapshot:
#if WITH_DEV_AUTOMATION_TESTS
static TAutoConsoleVariable<int32> CVarSkipClientResolveShotForTest(
TEXT("game.NetTest.SkipClientResolveShot"),
0,
TEXT("1 skips ClientResolveShot in test builds."));
#endif
Then a property-only convergence test should prove that the client actually had predictions to resolve:
100 local predictions enqueued
Pending before snapshot apply == 100
Authority results == 100
ClientResolveShot received == 0
Snapshot source/epoch == test source/epoch
Snapshot contiguous prefix reaches the final issued sequence
Pending removed by snapshot == 100
Pending removed by timeout == 0
Pending after snapshot apply == 0
Displayed ammo == authoritative ammo
CVar/barrier/timeout settings restored during teardown
Freeze prediction timeout while the snapshot is deliberately blocked, otherwise timeout removal can create a false positive.
For transport degradation, PktLoss, PktIncomingLoss, PktDup, and PktOrder are useful [28], but inject duplicate keys directly to test gameplay-handler idempotence.
Use Networking Insights before changing architecture
Use Networking Insights/Network Profiler to identify expensive Actors, properties, and RPCs [29,30]. "Networking is heavy" is not actionable; "this UI array replicates every tick to every connection" is.
A practical starting matrix
These are evaluation starting points, not Unreal Engine limits.
| Game shape | Baseline to prototype | Evaluate next |
|---|---|---|
| 2-4 player co-op | Built-in Replication + Character Movement + Listen Server | Steam/EOS P2P/relay, whether host migration is worth it |
| 8-16 player PvP | Dedicated Server + built-in Replication | GAS, bounded lag compensation, hosting platform |
| 32-100 players / larger maps | Dedicated Server + spatial relevancy | Generic + Replication Graph or Iris, Fast Arrays, autoscaling |
| Very large Actor counts | Dedicated Server + bot load tests | Compare Replication Graph and Iris as separate NetDriver configurations |
Implementation order I would use
- Connect two clients to a minimal Dedicated Server C++ project.
- Make movement, possession, death, respawn, HP, weapon, and ammo correct.
- Split shooting into local prediction and authoritative resolution; add ownership checks, validation, and rate limits.
- Fix late join and relevancy re-entry, then make latency/loss tests routine.
- Add Steam/EOS login, sessions, and invites.
- Run a packaged server remotely and load it with bots while measuring frame time and bandwidth.
- Evaluate GAS, Replication Graph, or Iris only for measured problems.
- Integrate allocation, health, and shutdown with the hosting platform.
Final takeaways
- Start gameplay synchronization with UE Replication; treat Steam/EOS as online services and GameLift/PlayFab/Agones as server operations.
- Let
UCharacterMovementComponenthandle normal Character prediction; replicate reconstructable state and use transient RPCs selectively. - Make shooting server-authoritative, keyed by source/epoch/sequence, with an immediate owner result plus owner-only replicated convergence state.
- Keep predicted owner FX separate from confirmed remote FX, and make every entry point idempotent.
- Add GAS, Replication Graph, Iris, or external networking SDKs only after fixed-version load tests show what problem you are actually solving.
References
- Networking and Multiplayer in Unreal Engine
- Setting Up Dedicated Servers in Unreal Engine
- Remote Procedure Calls in Unreal Engine
- Replicate Actor Properties in Unreal Engine
- Understanding Networked Movement in the Character Movement Component
- Actor Relevancy in Unreal Engine
- Actor Network Dormancy in Unreal Engine
- Actor Priority in Unreal Engine
- Detailed Actor Replication Flow
- Replication Graph in Unreal Engine
- Iris Replication System in Unreal Engine
- Network Prediction API
- Using Gameplay Abilities in Unreal Engine
- Abilities in Lyra
- Fast Array Serializer API
- Replicating UObjects in Unreal Engine
- Overview of Online Services in Unreal Engine
- Online Subsystem EOS Plugin
- Online Subsystem Steam
- Using Steam Sockets
- Common User Plugin in Lyra
- Photon Fusion for Unreal Introduction
- Photon Fusion for Unreal SDK Download
- Amazon GameLift Servers Unreal Integration
- PlayFab Multiplayer Servers Unreal GSDK
- Agones Overview
- Agones Unreal Client SDK
- Using Network Emulation in Unreal Engine
- Networking Insights in Unreal Engine
- Using the Network Profiler
- Game Mode and Game State in Unreal Engine
- Actor Owner and Owning Connection
- Replicated Object Execution Order
- Unreal Engine 5.8 Release Notes
- Get Server World Time Seconds
- Unreal Engine Console Commands Reference
- Migrate to Iris in Unreal Engine
- Iris Filtering in Unreal Engine
- Iris Prioritization in Unreal Engine
- Photon Fusion Unreal Property Replication
- UE 5.8.1 Hotfix Released
- UE 5.8.2 Hotfix Released
Top comments (0)