Skip to main content

Finding the Best Region for a Player

GameFlow exposes lightweight ping endpoints for each region so game clients can measure latency and select the best region for a match. Both HTTP and UDP are supported.

Ping Endpoints

Each region exposes a ping endpoint over two protocols:

  • HTTP, GET returns 200 OK with minimal payload. Simple, works everywhere, subject to TLS/TCP handshake overhead.
  • UDP, echo server on port 8080. Sends back packet payload verbatim. Closer to real gameplay traffic, no handshake, lower overhead.
RegionHTTPUDP
US Easthttps://us-east.ping.gameflow.ggus-east.ping.gameflow.gg:8080
US Westhttps://us-west.ping.gameflow.ggus-west.ping.gameflow.gg:8080
EU Centralhttps://eu-central.ping.gameflow.ggeu-central.ping.gameflow.gg:8080
AP Southeasthttps://ap-southeast.ping.gameflow.ggap-southeast.ping.gameflow.gg:8080

Prefer UDP for matchmaking decisions, measured RTT matches what the game server connection will experience. Use HTTP when UDP is blocked (restrictive networks, browsers).

Measuring Latency

Send a request to each endpoint and measure round-trip time. For UDP, send a small payload and wait for the echo. Compare results to pick the lowest-latency region.

HTTP

#include "HttpModule.h"
#include "Interfaces/IHttpRequest.h"
#include "Interfaces/IHttpResponse.h"

void UMySubsystem::PingRegion(const FString& Region)
{
FString Url = FString::Printf(TEXT("https://%s.ping.gameflow.gg"), *Region);

double StartTime = FPlatformTime::Seconds();

TSharedRef<IHttpRequest> Request = FHttpModule::Get().CreateRequest();
Request->SetURL(Url);
Request->SetVerb(TEXT("GET"));
Request->OnProcessRequestComplete().BindLambda(
[StartTime, Region](FHttpRequestPtr Req, FHttpResponsePtr Res, bool bSuccess)
{
if (bSuccess && Res.IsValid())
{
double Ms = (FPlatformTime::Seconds() - StartTime) * 1000.0;
UE_LOG(LogTemp, Log, TEXT("Ping to %s: %.0f ms"), *Region, Ms);
}
});
Request->ProcessRequest();
}

// Ping all regions
void UMySubsystem::PingAllRegions()
{
TArray<FString> Regions = {
TEXT("us-east"), TEXT("us-west"),
TEXT("eu-central"), TEXT("ap-southeast")
};
for (const FString& Region : Regions)
{
PingRegion(Region);
}
}

UDP

Send a small packet to port 8080 and time the echo. No handshake, so RTT reflects raw network latency.

#include "Sockets.h"
#include "SocketSubsystem.h"
#include "Interfaces/IPv4/IPv4Address.h"
#include "IPAddress.h"

void UMySubsystem::PingRegionUDP(const FString& Region)
{
ISocketSubsystem* SS = ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM);
FString Host = FString::Printf(TEXT("%s.ping.gameflow.gg"), *Region);

auto Addr = SS->GetAddressFromString(Host);
if (!Addr.IsValid()) return;
Addr->SetPort(8080);

FSocket* Socket = SS->CreateSocket(NAME_DGram, TEXT("ping"), false);
const uint8 Payload[] = { 'p', 'i', 'n', 'g' };
int32 Sent = 0;

double Start = FPlatformTime::Seconds();
Socket->SendTo(Payload, sizeof(Payload), Sent, *Addr);

uint8 Buf[64];
int32 Read = 0;
Socket->Wait(ESocketWaitConditions::WaitForRead, FTimespan::FromSeconds(2.0));
Socket->RecvFrom(Buf, sizeof(Buf), Read, *Addr);

double Ms = (FPlatformTime::Seconds() - Start) * 1000.0;
UE_LOG(LogTemp, Log, TEXT("UDP ping %s: %.0f ms"), *Region, Ms);

SS->DestroySocket(Socket);
}

Using the Best Region

Once you've determined the best region, pass it when requesting a game server via the GameFlow API. There are two approaches depending on your setup.

Standalone Server

Start a one-off game server with POST /v1/games/:gameId/servers. GameFlow provisions the server and blocks until it's ready, returning its IP and port.

const bestRegion = (await pingAllRegions())[0].region;

const response = await fetch(
`https://api.gameflow.gg/v1/games/${gameId}/servers`,
{
method: "POST",
headers: { "X-Api-Key": apiKey },
body: JSON.stringify({
region: bestRegion,
buildId: "build_abc123",
payload: JSON.stringify({ players, teamA, teamB }),
}),
}
);

const { server } = await response.json();
// server.address, server.port

Fleet Allocation (Autoscaling)

If you have autoscaling configured via a fleet, allocate a pre-warmed server with POST /v1/fleets/:gameId/allocate. This returns an already-running server from the pool, so it's faster than starting a standalone server.

const bestRegion = (await pingAllRegions())[0].region;

const response = await fetch(
`https://api.gameflow.gg/v1/fleets/${gameId}/allocate`,
{
method: "POST",
headers: { "X-Api-Key": apiKey },
body: JSON.stringify({
region: bestRegion,
payload: JSON.stringify({ players, teamA, teamB }),
}),
}
);

const { allocation } = await response.json();
// allocation.address, allocation.port

See the Custom Game Backend guide for a full integration walkthrough.

Best Practices

  • Prefer UDP when available, matches real gameplay traffic and avoids TLS/TCP handshake overhead. Fall back to HTTP when UDP is blocked (browsers, restrictive corporate networks).
  • Ping on game launch or lobby entry, avoid pinging mid-match to reduce unnecessary network traffic.
  • Let the player override, display the results and allow the player to manually select a region if they prefer.
  • Discard the first ping, HTTP includes TLS handshake overhead; UDP may warm up DNS/routing caches. Drop the first sample and average the rest.
  • Ping multiple times and average, a single request can be affected by cold connections or transient network conditions. Averaging 3-5 pings per region gives a more stable result.
  • Set a timeout on UDP, UDP does not guarantee delivery. Treat no-response within ~2s as a failed sample and retry or mark region unreachable.