Skip to content
UCIK Docs
Esc
↑↓navigate↵open⌘Jpreview
On this page

Matchmaking with Edgegap

Match players and receive the connection details for their server.

First publish and test your server. Configure an Edgegap matchmaker profile with the player count, matching rules, and application version it should deploy.

Setup

In Betide → Crossplay → Edgegap, set Matchmaker URL and its Auth Token. Their environment overrides are EDGEGAP_MM_URL and EDGEGAP_MATCHMAKER_KEY.

Use the matchmaker’s client token, not the main Edgegap API key. The client token is stored in project configuration and is intended for distribution with the game. Edgegap token and group model

Select Edgegap in the Matchmaking policy. The queue name must exactly match a remote profile; local player-count and skill settings do not rewrite that profile.

Queue one player

Unified queue-to-travel example blocked: matchmaking replaces the host with a complete host:port URL while retaining its port. Travel then appends that port again. The shared matchmaking result needs fixing before this flow is usable.

Find an Edgegap match
Start Matchmaking · Cancel Matchmaking · Travel To Crossplay Connect Info

Blueprint example

The adapter creates a ready group for one player. MATCH_FOUND waits for allocation; HOST_ASSIGNED provides the destination, with an external port named gameport. Do not request a second server. Edgegap assignment timing

Cancel Matchmaking stops the local queue but discards the remote deletion result. A cancellation event does not confirm that Edgegap removed the group.

Parties and ready-up

Run this Game Instance flow on each player’s client. The owner calls Create Edgegap Party, shares GroupId, and invitees call Join Edgegap Party. Each player calls Set Party Ready; the owner should ready last, after everyone has joined.

Use your exact profile name and an Attributes Json object matching its rules ({} only if none are required). Requests use the configured matchmaker URL and client token.

Party matchmaking
Preparing your graph…

Loading the interactive viewer.

Game Instance variables: PartyBusy, PartyOwner (Boolean, false); GroupId, MemberId (String); PartySnapshot (EGIK Group Up Member Response); PollTimer (Timer Handle).

Game module dependencies: EdgegapIntegrationKit, BetideCore, Engine.

// Copyright (c) 2026 Betide Studio. All Rights Reserved.
// EdgegapPartyGameInstance.h
#pragma once
#include "Engine/GameInstance.h"
#include "Engine/EngineBaseTypes.h"
#include "Nodes/GroupUp/EGIK_CreateGroup.h"
#include "Nodes/GroupUp/EGIK_CreateGroupMember.h"
#include "Nodes/GroupUp/EGIK_UpdateGroupMember.h"
#include "Nodes/GroupUp/EGIK_GetGroupMember.h"
#include "Nodes/GroupUp/EGIK_DeleteGroup.h"
#include "Nodes/GroupUp/EGIK_DeleteGroupMember.h"
#include "TimerManager.h"
#include "EdgegapPartyGameInstance.generated.h"

UCLASS()
class UEdgegapPartyGameInstance : public UGameInstance
{
    GENERATED_BODY()
  public:
    virtual void Init() override;
    virtual void Shutdown() override;
    void CreateEdgegapParty(const FString &Profile, const FString &AttributesJson);
    void JoinEdgegapParty(const FString &ExistingGroupId, const FString &Profile,
                          const FString &AttributesJson);
    void SetPartyReady(bool Ready);
    void RefreshParty();
    void LeaveEdgegapParty();
    void ConnectPartyMatch();
    void StartPartyPolling();
    void ForgetPartyAfterConfirmedExpiry();
    FString GroupId;
    FString MemberId;
    FEGIK_GroupUpMemberResponse PartySnapshot;

  private:
    enum class EOperation
    {
        Create,
        Join,
        Update,
        Read,
        Delete
    };
    EOperation Operation = EOperation::Read;
    bool PartyBusy = false;
    bool PartyOwner = false;
    FTimerHandle PollTimer;
    UPROPERTY()
    TObjectPtr<UEGIK_WebRequest> Action;
    bool BeginRequest(bool WantsGroup);
    void StopPolling();
    void ApplyPartySnapshot(const FEGIK_GroupUpMemberResponse &Response);
    UFUNCTION()
    void MemberSucceeded(const FEGIK_GroupUpMemberResponse &Response, const FBetideError &Error);
    UFUNCTION()
    void MemberFailed(const FEGIK_GroupUpMemberResponse &Response, const FBetideError &Error);
    UFUNCTION()
    void DeleteSucceeded(const FBetideError &Error);
    UFUNCTION()
    void RequestFailed(const FBetideError &Error);
    void NetworkFailed(UWorld *World, UNetDriver *Driver, ENetworkFailure::Type Failure,
                       const FString &Reason);
    void TravelFailed(UWorld *World, ETravelFailure::Type Failure, const FString &Reason);
    template <class T> void StartMemberAction(T *Node, EOperation NewOperation)
    {
        Operation = NewOperation;
        Action = Node;
        Node->RegisterWithGameInstance(this);
        Node->OnSuccess.AddDynamic(this, &ThisClass::MemberSucceeded);
        Node->OnFailure.AddDynamic(this, &ThisClass::MemberFailed);
        Node->OnRateLimited.AddDynamic(this, &ThisClass::MemberFailed);
        Node->Activate();
    }
    template <class T> void StartDeleteAction(T *Node)
    {
        Operation = EOperation::Delete;
        Action = Node;
        Node->RegisterWithGameInstance(this);
        Node->OnSuccess.AddDynamic(this, &ThisClass::DeleteSucceeded);
        Node->OnFailure.AddDynamic(this, &ThisClass::RequestFailed);
        Node->OnRateLimited.AddDynamic(this, &ThisClass::RequestFailed);
        Node->Activate();
    }
};

// EdgegapPartyGameInstance.cpp
#include "EdgegapPartyGameInstance.h"
#include "Engine/Engine.h"
#include "Engine/World.h"
#include "Kismet/GameplayStatics.h"
#include "UObject/Class.h"
DEFINE_LOG_CATEGORY_STATIC(LogGameEdgegapParty, Log, All);

void UEdgegapPartyGameInstance::Init()
{
    Super::Init();
    if (GEngine)
    {
        GEngine->OnNetworkFailure().AddUObject(this, &ThisClass::NetworkFailed);
        GEngine->OnTravelFailure().AddUObject(this, &ThisClass::TravelFailed);
    }
}
void UEdgegapPartyGameInstance::Shutdown()
{
    StopPolling();
    if (GEngine)
    {
        GEngine->OnNetworkFailure().RemoveAll(this);
        GEngine->OnTravelFailure().RemoveAll(this);
    }
    Super::Shutdown();
}
bool UEdgegapPartyGameInstance::BeginRequest(bool WantsGroup)
{
    if (PartyBusy)
    {
        UE_LOG(LogGameEdgegapParty, Warning,
               TEXT("A party request is running; retry after it finishes"));
        return false;
    }
    if (WantsGroup == GroupId.IsEmpty())
    {
        UE_LOG(LogGameEdgegapParty, Warning, TEXT("%s"),
               WantsGroup ? TEXT("Join or create a group first")
                          : TEXT("Leave the current group first"));
        return false;
    }
    PartyBusy = true;
    return true;
}
void UEdgegapPartyGameInstance::CreateEdgegapParty(const FString &Profile,
                                                   const FString &AttributesJson)
{
    if (!BeginRequest(false))
        return;
    PartyOwner = true;
    FEGIK_CreateGroupRequest Request;
    Request.Profile = Profile;
    Request.Attributes = AttributesJson;
    Request.bIsReady = false;
    StartMemberAction(UEGIK_CreateGroup::CreateGroup(Request), EOperation::Create);
}
void UEdgegapPartyGameInstance::JoinEdgegapParty(const FString &ExistingGroupId,
                                                 const FString &Profile,
                                                 const FString &AttributesJson)
{
    if (!BeginRequest(false))
        return;
    PartyOwner = false;
    FEGIK_CreateGroupMemberRequest Request;
    Request.GroupId = ExistingGroupId;
    Request.Profile = Profile;
    Request.Attributes = AttributesJson;
    Request.bIsReady = false;
    StartMemberAction(UEGIK_CreateGroupMember::CreateGroupMember(Request), EOperation::Join);
}
void UEdgegapPartyGameInstance::SetPartyReady(bool Ready)
{
    if (!BeginRequest(true))
        return;
    FEGIK_UpdateGroupMemberRequest Request;
    Request.GroupId = GroupId;
    Request.MemberId = MemberId;
    Request.bIsReady = Ready;
    StartMemberAction(UEGIK_UpdateGroupMember::UpdateGroupMember(Request), EOperation::Update);
}
void UEdgegapPartyGameInstance::RefreshParty()
{
    if (!BeginRequest(true))
        return;
    FEGIK_GetGroupMemberRequest Request;
    Request.GroupId = GroupId;
    Request.MemberId = MemberId;
    StartMemberAction(UEGIK_GetGroupMember::GetGroupMember(Request), EOperation::Read);
}
void UEdgegapPartyGameInstance::StartPartyPolling()
{
    GetTimerManager().SetTimer(PollTimer, this, &ThisClass::RefreshParty, 3.0f, true);
}
void UEdgegapPartyGameInstance::StopPolling()
{
    GetTimerManager().ClearTimer(PollTimer);
}
void UEdgegapPartyGameInstance::MemberSucceeded(const FEGIK_GroupUpMemberResponse &Response,
                                                const FBetideError &)
{
    Action = nullptr;
    if (Operation == EOperation::Create || Operation == EOperation::Join)
    {
        GroupId = Response.GroupId;
        MemberId = Response.MemberId;
        StartPartyPolling();
        PartyBusy = false;
        UE_LOG(LogGameEdgegapParty, Log,
               TEXT("Party joined unready; share GroupId and ready each member"));
        return;
    }
    StartPartyPolling();
    ApplyPartySnapshot(Response);
}
void UEdgegapPartyGameInstance::ApplyPartySnapshot(const FEGIK_GroupUpMemberResponse &Response)
{
    PartySnapshot = Response;
    PartyBusy = false;
    UE_LOG(LogGameEdgegapParty, Log, TEXT("%s"), *Response.Status);
    if (Response.Status == TEXT("HOST_ASSIGNED") || Response.Status == TEXT("CANCELLED"))
        StopPolling();
}
void UEdgegapPartyGameInstance::MemberFailed(const FEGIK_GroupUpMemberResponse &,
                                             const FBetideError &Error)
{
    RequestFailed(Error);
}
void UEdgegapPartyGameInstance::RequestFailed(const FBetideError &Error)
{
    Action = nullptr;
    PartyBusy = false;
    StopPolling();
    UE_LOG(LogGameEdgegapParty, Warning, TEXT("%s"), *Error.Message);
}
void UEdgegapPartyGameInstance::LeaveEdgegapParty()
{
    if (!BeginRequest(true))
        return;
    StopPolling();
    if (PartyOwner)
    {
        FEGIK_DeleteGroupRequest Request;
        Request.GroupId = GroupId;
        StartDeleteAction(UEGIK_DeleteGroup::DeleteGroup(Request));
    }
    else
    {
        FEGIK_DeleteGroupMemberRequest Request;
        Request.GroupId = GroupId;
        Request.MemberId = MemberId;
        StartDeleteAction(UEGIK_DeleteGroupMember::DeleteGroupMember(Request));
    }
}
void UEdgegapPartyGameInstance::DeleteSucceeded(const FBetideError &)
{
    Action = nullptr;
    UE_LOG(LogGameEdgegapParty, Log, TEXT("%s"),
           PartyOwner ? TEXT("Group closed") : TEXT("Membership removed"));
    GroupId.Reset();
    MemberId.Reset();
    PartySnapshot = FEGIK_GroupUpMemberResponse();
    PartyOwner = false;
    PartyBusy = false;
}
void UEdgegapPartyGameInstance::ConnectPartyMatch()
{
    if (!BeginRequest(true))
        return;
    if (PartySnapshot.Status != TEXT("HOST_ASSIGNED"))
    {
        PartyBusy = false;
        UE_LOG(LogGameEdgegapParty, Warning, TEXT("Wait for HOST_ASSIGNED before connecting"));
        return;
    }
    StopPolling();
    FString Error;
    const bool Accepted = UEGIKBlueprintFunctionLibrary::TravelPlayerToMatchAssignment(
        UGameplayStatics::GetPlayerController(this, 0), PartySnapshot.Assignment, Error);
    PartyBusy = false;
    if (Accepted)
        UE_LOG(LogGameEdgegapParty, Log, TEXT("Travel requested; wait for the server connection"));
    else
        UE_LOG(LogGameEdgegapParty, Warning, TEXT("%s"), *Error);
}
void UEdgegapPartyGameInstance::NetworkFailed(UWorld *World, UNetDriver *,
                                              ENetworkFailure::Type Failure, const FString &)
{
    if (World && World->GetGameInstance() != this)
        return;
    UE_LOG(LogGameEdgegapParty, Warning, TEXT("%s"), *UEnum::GetValueAsString(Failure));
}
void UEdgegapPartyGameInstance::TravelFailed(UWorld *World, ETravelFailure::Type Failure,
                                             const FString &)
{
    if (World && World->GetGameInstance() != this)
        return;
    UE_LOG(LogGameEdgegapParty, Warning, TEXT("%s"), *UEnum::GetValueAsString(Failure));
}

void UEdgegapPartyGameInstance::ForgetPartyAfterConfirmedExpiry()
{
    if (PartyBusy)
    {
        UE_LOG(LogGameEdgegapParty, Warning,
               TEXT("Wait for the active request before forgetting the party"));
        return;
    }
    StopPolling();
    GroupId.Reset();
    MemberId.Reset();
    PartySnapshot = FEGIK_GroupUpMemberResponse();
    PartyOwner = false;
    UE_LOG(LogGameEdgegapParty, Log, TEXT("Local party cleared; no remote delete was sent"));
}

Polling runs every three seconds and stops on HOST_ASSIGNED, CANCELLED, or an error. Read PartySnapshot for readiness, ticket ID, status, and assignment. After a transient error or rate limit, wait before calling Refresh Party to resume. A busy request must finish before another operation can start.

Call Connect Party Match after HOST_ASSIGNED, using an IP-compatible GameNetDriver and a local Player Controller. Success only means travel was requested; NetworkError and TravelError report later failures. Retry connection only after failure, with a limit appropriate for your server startup time. Use the member’s ticket ID in your game’s admission flow if required; travel does not send it automatically.

Leave Edgegap Party deletes the whole group for its creator and only the membership for an invitee. IDs stay available if deletion fails. After confirming that a membership expired or was removed, Forget Party After Confirmed Expiry clears local state; it does not delete anything remotely. Leave before shutting down the game rather than relying on an exit-time request.

The unified adapter’s party ready-up path remains incomplete; this example uses the direct Group Up nodes.

Attributes and placement

Match the profile’s attribute names and types. Group Up offers Structured Attributes or a raw JSON Attributes object; use one, not both. Player Ip is available on the direct group/member requests.

The unified adapter sends Config.Attributes as shared attributes. It does not serialize Config.LatencyHints or player IPs into its group requests. If your profile needs location or latency data, provide the profile’s expected attributes through the direct Group Up flow. Region labels alone are not measured latency.

Backfill an assigned server

This dedicated-server Game Instance handles one team’s backfill at a time. Pass Start Team Backfill an EGIK Create Back Fill Request:

Input Value
Profile Your backfill-compatible matchmaker profile
Deployment Request ID The running deployment’s ID
Assignment Details Its full assignment JSON: fqdn, public_ip, ports, location
Tickets At least one connected team member: ticket ID → full ticket JSON, not only attributes
Attributes Override Optional profile attributes; omit the reserved assignment key

For a profile using backfill_group_size, update the existing tickets’ values to the group sizes that fit the team’s remaining seats. Use actual roster data from the matchmaker’s injected variables and prior backfills. Edgegap backfill rules

Leave Matchmaking URL and Auth Token empty to use configuration. If overridden, this example keeps them for polling and deletion.

Replace missing players
Preparing your graph…

Loading the interactive viewer.

Game Instance variables: BackfillBusy (Boolean, false); BackfillId, BackfillStatus (String); ActiveRequest (EGIK Create Back Fill Request); AssignedTicket (EGIK Matchmaking Response); BackfillTimer (Timer Handle).

Server module dependencies: EdgegapIntegrationKit, BetideCore, Engine.

// Copyright (c) 2026 Betide Studio. All Rights Reserved.
// EdgegapBackfillGameInstance.h
#pragma once
#include "Engine/GameInstance.h"
#include "Nodes/Backfills/EGIK_CreateBackfill.h"
#include "Nodes/Backfills/EGIK_GetBackFillTicketInformation.h"
#include "Nodes/Backfills/EGIK_DeleteBackFillTicket.h"
#include "TimerManager.h"
#include "EdgegapBackfillGameInstance.generated.h"

UCLASS()
class UEdgegapBackfillGameInstance : public UGameInstance
{
    GENERATED_BODY()
  public:
    virtual void Shutdown() override;
    void StartTeamBackfill(const FEGIK_CreateBackFillRequest &Request);
    void RefreshBackfill();
    void DeleteTeamBackfill();
    void ForgetBackfillAfterConfirmedRemoval();
    FString BackfillId;
    FString BackfillStatus;
    FEGIK_MatchmakingResponse AssignedTicket;

  private:
    bool BackfillBusy = false;
    FTimerHandle BackfillTimer;
    FEGIK_CreateBackFillRequest ActiveRequest;
    UPROPERTY()
    TObjectPtr<UEGIK_WebRequest> Action;
    bool BeginRequest(bool NeedsId);
    void ApplyBackfillResult(const FString &Id, const FString &Status,
                             const FEGIK_MatchmakingResponse &Replacement);
    void ClearLocalBackfill();
    UFUNCTION()
    void Created(const FEGIK_CreateBackFillResponse &Response, const FBetideError &Error);
    UFUNCTION()
    void CreateFailed(const FEGIK_CreateBackFillResponse &Response, const FBetideError &Error);
    UFUNCTION()
    void Refreshed(const FEGIK_GetBackFillTicketInformationResponse &Response,
                   const FBetideError &Error);
    UFUNCTION()
    void RefreshFailed(const FEGIK_GetBackFillTicketInformationResponse &Response,
                       const FBetideError &Error);
    UFUNCTION()
    void Deleted(const FBetideError &Error);
    UFUNCTION()
    void RequestFailed(const FBetideError &Error);
};

// EdgegapBackfillGameInstance.cpp
#include "EdgegapBackfillGameInstance.h"
#include "Kismet/KismetSystemLibrary.h"
DEFINE_LOG_CATEGORY_STATIC(LogGameEdgegapBackfill, Log, All);

void UEdgegapBackfillGameInstance::Shutdown()
{
    GetTimerManager().ClearTimer(BackfillTimer);
    Super::Shutdown();
}
bool UEdgegapBackfillGameInstance::BeginRequest(bool NeedsId)
{
    if (!UKismetSystemLibrary::IsDedicatedServer(this))
    {
        UE_LOG(LogGameEdgegapBackfill, Warning, TEXT("Run backfill on the dedicated server"));
        return false;
    }
    if (BackfillBusy)
    {
        UE_LOG(LogGameEdgegapBackfill, Warning,
               TEXT("Backfill request running; retry after completion"));
        return false;
    }
    if (NeedsId == BackfillId.IsEmpty())
    {
        UE_LOG(LogGameEdgegapBackfill, Warning, TEXT("%s"),
               NeedsId ? TEXT("No active backfill ID")
                       : TEXT("Delete the current backfill before creating another"));
        return false;
    }
    BackfillBusy = true;
    return true;
}
void UEdgegapBackfillGameInstance::StartTeamBackfill(const FEGIK_CreateBackFillRequest &Request)
{
    if (!BeginRequest(false))
        return;
    ActiveRequest = Request;
    if (Request.Tickets.IsEmpty())
    {
        BackfillBusy = false;
        UE_LOG(LogGameEdgegapBackfill, Warning,
               TEXT("Supply at least one current team ticket with its full JSON data"));
        return;
    }
    auto *Node = UEGIK_CreateBackfill::CreateBackFillRequest(Request);
    Action = Node;
    Node->RegisterWithGameInstance(this);
    Node->OnSuccess.AddDynamic(this, &ThisClass::Created);
    Node->OnFailure.AddDynamic(this, &ThisClass::CreateFailed);
    Node->OnRateLimited.AddDynamic(this, &ThisClass::CreateFailed);
    Node->Activate();
}
void UEdgegapBackfillGameInstance::Created(const FEGIK_CreateBackFillResponse &Response,
                                           const FBetideError &)
{
    ApplyBackfillResult(Response.Id, Response.Status, Response.AssignedTicket);
}
void UEdgegapBackfillGameInstance::CreateFailed(const FEGIK_CreateBackFillResponse &,
                                                const FBetideError &Error)
{
    RequestFailed(Error);
}
void UEdgegapBackfillGameInstance::RefreshBackfill()
{
    if (!BeginRequest(true))
        return;
    auto *Node = UEGIK_GetBackFillTicketInformation::GetBackFillTicketInformation(
        BackfillId, ActiveRequest.MatchmakingURL, ActiveRequest.AuthToken);
    Action = Node;
    Node->RegisterWithGameInstance(this);
    Node->OnSuccess.AddDynamic(this, &ThisClass::Refreshed);
    Node->OnFailure.AddDynamic(this, &ThisClass::RefreshFailed);
    Node->OnRateLimited.AddDynamic(this, &ThisClass::RefreshFailed);
    Node->Activate();
}
void UEdgegapBackfillGameInstance::Refreshed(
    const FEGIK_GetBackFillTicketInformationResponse &Response, const FBetideError &)
{
    ApplyBackfillResult(Response.Id, Response.Status, Response.AssignedTicket);
}
void UEdgegapBackfillGameInstance::RefreshFailed(const FEGIK_GetBackFillTicketInformationResponse &,
                                                 const FBetideError &Error)
{
    RequestFailed(Error);
}
void UEdgegapBackfillGameInstance::ApplyBackfillResult(const FString &Id, const FString &Status,
                                                       const FEGIK_MatchmakingResponse &Replacement)
{
    Action = nullptr;
    BackfillId = Id;
    BackfillStatus = Status;
    AssignedTicket = Replacement;
    BackfillBusy = false;
    if (!Replacement.TicketId.IsEmpty())
    {
        GetTimerManager().ClearTimer(BackfillTimer);
        UE_LOG(LogGameEdgegapBackfill, Log,
               TEXT("Replacement ticket available; verify its player connection before counting "
                    "the seat as filled"));
        return;
    }
    if (Status == TEXT("CANCELLED"))
    {
        GetTimerManager().ClearTimer(BackfillTimer);
        UE_LOG(LogGameEdgegapBackfill, Warning,
               TEXT("Backfill cancelled; clear expired state only after confirming removal"));
        return;
    }
    GetTimerManager().SetTimer(BackfillTimer, this, &ThisClass::RefreshBackfill, 3.0f, true);
}
void UEdgegapBackfillGameInstance::RequestFailed(const FBetideError &Error)
{
    Action = nullptr;
    BackfillBusy = false;
    GetTimerManager().ClearTimer(BackfillTimer);
    UE_LOG(LogGameEdgegapBackfill, Warning, TEXT("%s"), *Error.Message);
}
void UEdgegapBackfillGameInstance::DeleteTeamBackfill()
{
    if (!BeginRequest(true))
        return;
    GetTimerManager().ClearTimer(BackfillTimer);
    auto *Node = UEGIK_DeleteBackFillTicket::DeleteBackFillTicket(
        BackfillId, ActiveRequest.MatchmakingURL, ActiveRequest.AuthToken);
    Action = Node;
    Node->RegisterWithGameInstance(this);
    Node->OnSuccess.AddDynamic(this, &ThisClass::Deleted);
    Node->OnFailure.AddDynamic(this, &ThisClass::RequestFailed);
    Node->OnRateLimited.AddDynamic(this, &ThisClass::RequestFailed);
    Node->Activate();
}
void UEdgegapBackfillGameInstance::ClearLocalBackfill()
{
    GetTimerManager().ClearTimer(BackfillTimer);
    BackfillId.Reset();
    BackfillStatus.Reset();
    AssignedTicket = FEGIK_MatchmakingResponse();
    ActiveRequest = FEGIK_CreateBackFillRequest();
}
void UEdgegapBackfillGameInstance::Deleted(const FBetideError &)
{
    Action = nullptr;
    ClearLocalBackfill();
    BackfillBusy = false;
    UE_LOG(LogGameEdgegapBackfill, Log, TEXT("Backfill removed"));
}
void UEdgegapBackfillGameInstance::ForgetBackfillAfterConfirmedRemoval()
{
    if (BackfillBusy)
    {
        UE_LOG(LogGameEdgegapBackfill, Warning, TEXT("Wait for the active request first"));
        return;
    }
    ClearLocalBackfill();
    UE_LOG(LogGameEdgegapBackfill, Log, TEXT("Local backfill cleared; no remote delete was sent"));
}

Polling stops when AssignedTicket is available, the backfill is cancelled, or a request fails. Verify the arriving player’s ticket against your server’s admission data; assignment alone does not fill a seat. Copy the assigned ticket into your roster before deleting or clearing this flow’s state.

Call Delete Team Backfill when the match closes or the request is no longer needed. Failure retains the ID for retry. After a transient error or rate limit, wait before Refresh Backfill. Use Forget Backfill After Confirmed Removal only after confirming expiry/removal; it clears local data without a remote delete. To fill further seats, start a new request with the updated roster.

Cancellation, recovery, and errors

For unified queues, Cancel Matchmaking deletes the adapter’s group. For direct Group Up, remove a departing member with Delete Group Member; Delete Group ends the whole group.

The unified adapter caches the group’s owner-member ID in memory. A saved group ID alone cannot resume its polling after restarting the game. For persistent party recovery, retain the direct API’s group and member IDs and handle expiry.

If stuck in SEARCHING, check profile criteria, enough compatible players, and each member’s readiness. If stuck in MATCH_FOUND, check deployment capacity and server startup. An expired or deleted ticket needs a new request. Show the returned FBetideError and back off after rate limits instead of polling every frame.

Was this page helpful?