MST
Technical Article Networking · Server Rewind
Networking · Unreal Engine 5.7

Server Rewinding for Fair Hits

Server rewind is not a magic engine toggle. It is a server-side historical query system that lets you validate a shot against the target’s past collision state. This refreshed guide turns the old simplified explanation into a cleaner UE5-oriented implementation strategy.

Updated For
UE5.7 multiplayer workflows
Refreshed around synchronized server time, network emulation, and modern rollback-oriented Unreal concepts.
Best For
Hitscan lag compensation
Most useful for validating shots or instant interactions where the player’s screen is showing a slightly older world.
Key Concepts
History · Interpolation · Clamp
The article focuses on building short history buffers, reconstructing frames safely, and validating within a bounded rewind window.
01 · Combat fairness

What rewind solves

Server rewinding—often called lag compensation or server-side rewind—exists to answer one question fairly: “What did the target look like when the shooter actually took the shot?”

That matters most for hitscan or near-instant interactions. Without rewind, high-latency players see a valid shot on their screen, but by the time the server evaluates it in the present, the target may already have moved away.

Best use case

Hitscan weapons, instant traces, or other actions where “I clicked on them” matters.

Less useful for

Server-simulated projectiles that already exist in authoritative world time.

What it is not

A replacement for client prediction. Rewind validates a past shot; prediction improves current input feel.

Why it is custom

The exact shapes, retention window, and fairness rules depend on your combat model.

In Unreal terms, it is helpful to separate three similar ideas that people often blur together:

  • client prediction makes local control responsive,
  • reconciliation corrects the owner back to authority,
  • server rewind validates a past interaction against historical target state.

Those systems often work together, but they solve different problems.

Practical framing
Think of rewind as a target history query on the server, not as a general “rewind the entire game” feature.
02 · Required data

Data you must record

A useful rewind implementation does not need a full copy of the world every tick. It needs the smallest amount of historical state required to validate the interaction.

Server time stamp

Use synchronized server time, not the client’s unsynchronized wall clock.

Historical collision state

Store hitboxes, capsule state, or other query shapes for the target.

Short retention window

Commonly a small circular window, for example the last 100–250 ms, depending on your design.

Stable query path

Be able to reconstruct a frame at an arbitrary past time, usually by interpolation between two samples.

That leads to a very important optimization choice: store query-friendly combat state, not everything. If your rewind query only needs a handful of hit volumes, record those. Recording every possible transient detail of the actor is usually wasted cost.

For a typical shooter or action game, the minimal rewind frame often contains:

  • a server timestamp,
  • a set of named hit volumes,
  • their world transforms,
  • and their extents.
03 · Historical state

Capturing rewind history

A lightweight history component is usually a good fit. It keeps the capture logic close to the actor and makes the retention window explicit.

Rewind frame dataHistory component types

USTRUCT()
struct FRewindHitBoxState
{
    GENERATED_BODY()

    UPROPERTY()
    FName Name;

    UPROPERTY()
    FTransform WorldTransform;

    UPROPERTY()
    FVector HalfExtent = FVector::ZeroVector;
};

USTRUCT()
struct FRewindFrame
{
    GENERATED_BODY()

    UPROPERTY()
    double ServerTimeSeconds = 0.0;

    UPROPERTY()
    TArray HitBoxes;
};

UCLASS(ClassGroup=Network, meta=(BlueprintSpawnableComponent))
class URewindHistoryComponent : public UActorComponent
{
    GENERATED_BODY()

public:
    virtual void TickComponent(
        float DeltaTime,
        ELevelTick TickType,
        FActorComponentTickFunction* ThisTickFunction) override;

    bool BuildFrameAtTime(double QueryTime, FRewindFrame& OutFrame) const;

protected:
    UPROPERTY(EditDefaultsOnly, Category="Rewind")
    double MaxRecordSeconds = 0.25;

    UPROPERTY(EditInstanceOnly, Category="Rewind")
    TArray> TrackedHitBoxes;

    UPROPERTY()
    TArray History;

    void RecordFrame(double ServerTimeSeconds);
    void TrimHistory(double CurrentServerTime);
};
Recording and reconstructing a frameServer-only history capture

void URewindHistoryComponent::TickComponent(
    float DeltaTime,
    ELevelTick TickType,
    FActorComponentTickFunction* ThisTickFunction)
{
    Super::TickComponent(DeltaTime, TickType, ThisTickFunction);

    if (!GetOwner() || !GetOwner()->HasAuthority())
    {
        return;
    }

    const AGameStateBase* GameState = GetWorld()->GetGameState();
    const double ServerTime = GameState
        ? GameState->GetServerWorldTimeSeconds()
        : GetWorld()->GetTimeSeconds();

    RecordFrame(ServerTime);
    TrimHistory(ServerTime);
}

void URewindHistoryComponent::RecordFrame(const double ServerTimeSeconds)
{
    FRewindFrame& Frame = History.AddDefaulted_GetRef();
    Frame.ServerTimeSeconds = ServerTimeSeconds;
    Frame.HitBoxes.Reserve(TrackedHitBoxes.Num());

    for (UBoxComponent* Box : TrackedHitBoxes)
    {
        if (!IsValid(Box))
        {
            continue;
        }

        FRewindHitBoxState& State = Frame.HitBoxes.AddDefaulted_GetRef();
        State.Name = Box->GetFName();
        State.WorldTransform = Box->GetComponentTransform();
        State.HalfExtent = Box->GetScaledBoxExtent();
    }
}

void URewindHistoryComponent::TrimHistory(const double CurrentServerTime)
{
    const double OldestAllowedTime = CurrentServerTime - MaxRecordSeconds;

    while (History.Num() > 0 && History[0].ServerTimeSeconds < OldestAllowedTime)
    {
        History.RemoveAt(0, 1, false);
    }
}

bool URewindHistoryComponent::BuildFrameAtTime(
    const double QueryTime,
    FRewindFrame& OutFrame) const
{
    if (History.Num() == 0)
    {
        return false;
    }

    const FRewindFrame* Older = nullptr;
    const FRewindFrame* Newer = nullptr;

    for (int32 Index = History.Num() - 1; Index >= 0; --Index)
    {
        if (History[Index].ServerTimeSeconds <= QueryTime)
        {
            Older = &History[Index];
            Newer = (Index + 1 < History.Num()) ? &History[Index + 1] : Older;
            break;
        }
    }

    if (Older == nullptr)
    {
        Older = &History[0];
        Newer = &History[0];
    }

    const double Denominator =
        FMath::Max(Newer->ServerTimeSeconds - Older->ServerTimeSeconds, KINDA_SMALL_NUMBER);
    const float Alpha =
        (Older == Newer) ? 0.0f : float((QueryTime - Older->ServerTimeSeconds) / Denominator);

    OutFrame.ServerTimeSeconds = QueryTime;
    OutFrame.HitBoxes.Reset(Older->HitBoxes.Num());

    for (int32 BoxIndex = 0; BoxIndex < Older->HitBoxes.Num(); ++BoxIndex)
    {
        const FRewindHitBoxState& A = Older->HitBoxes[BoxIndex];
        const FRewindHitBoxState& B =
            Newer->HitBoxes.IsValidIndex(BoxIndex) ? Newer->HitBoxes[BoxIndex] : A;

        FRewindHitBoxState& Result = OutFrame.HitBoxes.AddDefaulted_GetRef();
        Result.Name = A.Name;
        Result.HalfExtent = FMath::Lerp(A.HalfExtent, B.HalfExtent, Alpha);
        Result.WorldTransform.SetLocation(
            FMath::Lerp(A.WorldTransform.GetLocation(), B.WorldTransform.GetLocation(), Alpha));
        Result.WorldTransform.SetRotation(
            FQuat::Slerp(A.WorldTransform.GetRotation(), B.WorldTransform.GetRotation(), Alpha));
        Result.WorldTransform.SetScale3D(
            FMath::Lerp(A.WorldTransform.GetScale3D(), B.WorldTransform.GetScale3D(), Alpha));
    }

    return true;
}

The important part is not the exact container type. It is the short historical window and the ability to rebuild a frame at the requested time.

04 · Authoritative query

Validating a shot in the past

Once you have the target history, the actual server validation path becomes straightforward:

  1. receive the shot request,
  2. clamp the requested shot time,
  3. reconstruct the target’s collision state at that past moment,
  4. trace against the reconstructed state,
  5. restore present state,
  6. apply the authoritative result.
Authoritative rewind validationCore server flow

UFUNCTION(Server, Reliable)
void ServerConfirmHit(
    ACharacter* TargetCharacter,
    double ShotServerTime,
    FVector_NetQuantize TraceStart,
    FVector_NetQuantizeNormal ShotDirection);

void AMyWeapon::ServerConfirmHit_Implementation(
    ACharacter* TargetCharacter,
    double ShotServerTime,
    FVector_NetQuantize TraceStart,
    FVector_NetQuantizeNormal ShotDirection)
{
    if (!IsValid(TargetCharacter))
    {
        return;
    }

    URewindHistoryComponent* History =
        TargetCharacter->FindComponentByClass();

    if (!IsValid(History))
    {
        return;
    }

    const AGameStateBase* GameState = GetWorld()->GetGameState();
    const double Now = GameState
        ? GameState->GetServerWorldTimeSeconds()
        : GetWorld()->GetTimeSeconds();

    const double MaxRewindSeconds = 0.25;
    const double ClampedShotTime =
        FMath::Clamp(ShotServerTime, Now - MaxRewindSeconds, Now);

    FRewindFrame ReconstructedFrame;
    if (!History->BuildFrameAtTime(ClampedShotTime, ReconstructedFrame))
    {
        return;
    }

    FScopedRewindHitBoxes ScopedRewind(TargetCharacter, ReconstructedFrame);

    FHitResult Hit;
    FCollisionQueryParams Params(SCENE_QUERY_STAT(ServerRewind), false, GetOwner());

    const FVector TraceEnd =
        TraceStart + FVector(ShotDirection).GetSafeNormal() * 10000.0f;

    const bool bBlocked = GetWorld()->LineTraceSingleByChannel(
        Hit,
        TraceStart,
        TraceEnd,
        ECC_GameTraceChannel2,
        Params);

    if (bBlocked && Hit.GetActor() == TargetCharacter)
    {
        ApplyConfirmedDamage(TargetCharacter, Hit);
    }
}

There are two design details hidden inside that example that matter a lot:

  • Rewind only what you need. Usually that means the target’s hitboxes, not the entire world.
  • Restore immediately. Rewind is a temporary query state, not a persistent simulation change.
Healthy scope
A rewind query should feel like a temporary collision snapshot used for validation, not a broad time machine applied to all actors.
05 · Trust boundaries

Time sync and anti-cheat guards

The most dangerous input to trust here is time. The server should not blindly accept “I fired 800 ms ago” just because the client says so.

A much safer pattern is to send a synchronized server time value from the client, then clamp it on the server to your allowed rewind window. AGameStateBase::GetServerWorldTimeSeconds() is extremely useful here because Epic’s docs describe it as a synchronized server version of world time that is available on both client and server.

Clamp the window

Never allow an arbitrary rewind depth; keep it bounded to your design window.

Validate the payload

Check the target, trace origin, direction, and range against weapon rules and ownership.

Avoid trusting raw ping

Ping can inform limits, but it should not become an unchecked “accept any past time” bypass.

Prefer server-relative time

A synchronized server time stamp is safer than a free-running client clock.

Other good anti-cheat guards include:

  • weapon fire-rate validation,
  • range validation,
  • line-of-fire sanity checks,
  • and making sure the claimed shot time still fits inside your history buffer.

If any of those fail, the safest response is usually to reject the rewind request and let the server’s current-state rules win.

06 · Gameplay rules

Hitscan vs projectile design

Server rewind is most natural for hitscan. The shot is effectively an instantaneous query, so a historical collision test makes sense.

Weapon model Typical choice Why
Hitscan rifle Use rewind The authoritative question is “did the trace intersect the target at shot time?”
Server-simulated projectile Usually no rewind on impact The projectile already exists in server time and the server simulates its travel authoritatively.
Hybrid instant projectile feel Design-specific Some games still validate using historical target state, but the exact fairness rule is a design decision.

Another design choice is whether world geometry should be current or historical during the rewind query. Many implementations only rewind characters and keep the world current because it is cheaper and simpler, but that is a game rule, not a universal truth. Be explicit about it.

For example, if a door closed after the shot was taken, should the current closed door block the rewind trace or should the shot-time world state win? Different games answer that differently. The important thing is consistency.

07 · Verification

Testing and debugging

Like prediction, rewind needs bad-network testing, not just localhost testing. You want to know how it behaves when the firing client is behind, when targets are moving fast, and when packet loss introduces timing irregularities.

Useful tests include:

  • firing at strafing targets under 80–150 ms emulated latency,
  • verifying clamp behavior beyond the max rewind window,
  • testing edge cases at the oldest still-valid historical frame,
  • and checking that restored hitboxes always return to present state after the query.
Debug checklistThings worth visualizing

- Draw current hitboxes and rewound hitboxes in different colors.
- Log the requested shot time, the clamped shot time, and the oldest valid frame in history.
- Record whether the rewind frame came from an exact sample or interpolation.
- Assert that all temporary hitbox moves are restored before leaving the validation function.
- Test both dedicated server and listen server flows.
Easy bug to miss
A rewind system that validates correctly but occasionally fails to restore the present hitbox state will create impossible follow-up bugs elsewhere in combat.
08 · Sources

References and further reading

These official docs were the main references used to refresh the article and ground it in current Unreal networking guidance.