DEV Community

GameDevToolLab
GameDevToolLab

Posted on

Real-Time Networking for 3D FPS Games in UE5: Replication, EOS, Dedicated Servers, and Pitfalls

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:

  1. Gameplay networking — movement, shooting, damage, ammo, projectiles, doors, objectives, and match state.
  2. Online services — authentication, friends, invites, lobbies, sessions, matchmaking, voice, P2P, and relay.
  3. 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;
};
Enter fullscreen mode Exit fullscreen mode

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:

  1. Which network connection actually sent this Server RPC?
  2. 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(...)
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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:

  • FireSourceId
  • FireEpoch
  • LastContiguousResolvedSequence
  • 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);
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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.

  1. Relevancy — owner relevancy, cull distance, conditional replication [4,6,9].
  2. Update frequency — tune by Actor role; it is not the server simulation tick.
  3. Dormancy — wake/flush before changing replicated state [7].
  4. Priority — relative scheduling under saturation, not total bandwidth [8].
  5. 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

  1. Connect two clients to a minimal Dedicated Server C++ project.
  2. Make movement, possession, death, respawn, HP, weapon, and ammo correct.
  3. Split shooting into local prediction and authoritative resolution; add ownership checks, validation, and rate limits.
  4. Fix late join and relevancy re-entry, then make latency/loss tests routine.
  5. Add Steam/EOS login, sessions, and invites.
  6. Run a packaged server remotely and load it with bots while measuring frame time and bandwidth.
  7. Evaluate GAS, Replication Graph, or Iris only for measured problems.
  8. 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 UCharacterMovementComponent handle 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

  1. Networking and Multiplayer in Unreal Engine
  2. Setting Up Dedicated Servers in Unreal Engine
  3. Remote Procedure Calls in Unreal Engine
  4. Replicate Actor Properties in Unreal Engine
  5. Understanding Networked Movement in the Character Movement Component
  6. Actor Relevancy in Unreal Engine
  7. Actor Network Dormancy in Unreal Engine
  8. Actor Priority in Unreal Engine
  9. Detailed Actor Replication Flow
  10. Replication Graph in Unreal Engine
  11. Iris Replication System in Unreal Engine
  12. Network Prediction API
  13. Using Gameplay Abilities in Unreal Engine
  14. Abilities in Lyra
  15. Fast Array Serializer API
  16. Replicating UObjects in Unreal Engine
  17. Overview of Online Services in Unreal Engine
  18. Online Subsystem EOS Plugin
  19. Online Subsystem Steam
  20. Using Steam Sockets
  21. Common User Plugin in Lyra
  22. Photon Fusion for Unreal Introduction
  23. Photon Fusion for Unreal SDK Download
  24. Amazon GameLift Servers Unreal Integration
  25. PlayFab Multiplayer Servers Unreal GSDK
  26. Agones Overview
  27. Agones Unreal Client SDK
  28. Using Network Emulation in Unreal Engine
  29. Networking Insights in Unreal Engine
  30. Using the Network Profiler
  31. Game Mode and Game State in Unreal Engine
  32. Actor Owner and Owning Connection
  33. Replicated Object Execution Order
  34. Unreal Engine 5.8 Release Notes
  35. Get Server World Time Seconds
  36. Unreal Engine Console Commands Reference
  37. Migrate to Iris in Unreal Engine
  38. Iris Filtering in Unreal Engine
  39. Iris Prioritization in Unreal Engine
  40. Photon Fusion Unreal Property Replication
  41. UE 5.8.1 Hotfix Released
  42. UE 5.8.2 Hotfix Released

Top comments (0)