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.
Ping Endpoints
Each region has a dedicated ping endpoint that responds to HTTP GET requests. The response includes a 200 OK with minimal payload, making it ideal for round-trip time measurement.
| Region | Endpoint |
|---|---|
| US East | https://us-east.ping.gameflow.gg |
| US West | https://us-west.ping.gameflow.gg |
| EU Central | https://eu-central.ping.gameflow.gg |
| AP Southeast | https://ap-southeast.ping.gameflow.gg |
Measuring Latency
Send an HTTP GET request to each endpoint and measure the round-trip time. Compare the results to determine the lowest-latency region for the player.
- Unreal Engine
- Unity
- Godot
- JavaScript
- Go
- Rust
#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);
}
}
using System.Collections;
using System.Diagnostics;
using UnityEngine;
using UnityEngine.Networking;
public class RegionPing : MonoBehaviour
{
private static readonly string[] Regions = {
"us-east", "us-west", "eu-central", "ap-southeast"
};
public void PingAllRegions()
{
foreach (var region in Regions)
{
StartCoroutine(PingRegion(region));
}
}
private IEnumerator PingRegion(string region)
{
var url = $"https://{region}.ping.gameflow.gg";
var sw = Stopwatch.StartNew();
using var request = UnityWebRequest.Get(url);
yield return request.SendWebRequest();
sw.Stop();
if (request.result == UnityWebRequest.Result.Success)
{
UnityEngine.Debug.Log($"Ping to {region}: {sw.ElapsedMilliseconds} ms");
}
}
}
extends Node
const REGIONS = ["us-east", "us-west", "eu-central", "ap-southeast"]
func ping_all_regions():
for region in REGIONS:
ping_region(region)
func ping_region(region: String):
var http = HTTPRequest.new()
add_child(http)
http.request_completed.connect(_on_ping_completed.bind(region, http))
var url = "https://%s.ping.gameflow.gg" % region
var start = Time.get_ticks_msec()
http.set_meta("start_time", start)
http.request(url)
func _on_ping_completed(result: int, response_code: int, _headers: PackedStringArray, _body: PackedByteArray, region: String, http: HTTPRequest):
if result == HTTPRequest.RESULT_SUCCESS:
var ms = Time.get_ticks_msec() - http.get_meta("start_time")
print("Ping to %s: %d ms" % [region, ms])
http.queue_free()
const REGIONS = ["us-east", "us-west", "eu-central", "ap-southeast"];
async function pingRegion(region) {
const url = `https://${region}.ping.gameflow.gg`;
const start = performance.now();
await fetch(url);
const ms = Math.round(performance.now() - start);
console.log(`Ping to ${region}: ${ms} ms`);
return { region, ms };
}
async function pingAllRegions() {
const results = await Promise.all(REGIONS.map(pingRegion));
results.sort((a, b) => a.ms - b.ms);
console.log(`Best region: ${results[0].region} (${results[0].ms} ms)`);
return results;
}
package ping
import (
"fmt"
"net/http"
"sort"
"sync"
"time"
)
var Regions = []string{"us-east", "us-west", "eu-central", "ap-southeast"}
type Result struct {
Region string
Ms int64
}
func PingRegion(region string) (Result, error) {
url := fmt.Sprintf("https://%s.ping.gameflow.gg", region)
start := time.Now()
resp, err := http.Get(url)
if err != nil {
return Result{}, err
}
resp.Body.Close()
ms := time.Since(start).Milliseconds()
return Result{Region: region, Ms: ms}, nil
}
func PingAllRegions() ([]Result, error) {
var (
mu sync.Mutex
results []Result
wg sync.WaitGroup
)
for _, r := range Regions {
wg.Add(1)
go func(region string) {
defer wg.Done()
res, err := PingRegion(region)
if err != nil {
return
}
mu.Lock()
results = append(results, res)
mu.Unlock()
}(r)
}
wg.Wait()
sort.Slice(results, func(i, j int) bool { return results[i].Ms < results[j].Ms })
return results, nil
}
use std::time::Instant;
const REGIONS: &[&str] = &["us-east", "us-west", "eu-central", "ap-southeast"];
pub struct PingResult {
pub region: String,
pub ms: u128,
}
pub async fn ping_region(region: String) -> Result<PingResult, reqwest::Error> {
let url = format!("https://{}.ping.gameflow.gg", region);
let start = Instant::now();
reqwest::get(&url).await?;
let ms = start.elapsed().as_millis();
Ok(PingResult { region, ms })
}
pub async fn ping_all_regions() -> Vec<PingResult> {
let mut handles = Vec::new();
for ®ion in REGIONS {
handles.push(tokio::spawn(ping_region(region.to_string())));
}
let mut results = Vec::new();
for handle in handles {
if let Ok(Ok(result)) = handle.await {
results.push(result);
}
}
results.sort_by_key(|r| r.ms);
results
}
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.
- TypeScript / JavaScript
- Go
- Rust
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,
payload: JSON.stringify({ players, teamA, teamB }),
}),
}
);
const { server } = await response.json();
// server.address, server.port
results, _ := PingAllRegions()
bestRegion := results[0].Region
body, _ := json.Marshal(map[string]string{
"region": bestRegion,
"payload": `{"players":["p1","p2"]}`,
})
req, _ := http.NewRequest("POST",
fmt.Sprintf("https://api.gameflow.gg/v1/games/%s/servers", gameId),
bytes.NewReader(body),
)
req.Header.Set("X-Api-Key", apiKey)
req.Header.Set("Content-Type", "application/json")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
let results = ping_all_regions().await;
let best_region = &results[0].region;
let client = reqwest::Client::new();
let res = client
.post(format!(
"https://api.gameflow.gg/v1/games/{}/servers",
game_id
))
.header("X-Api-Key", api_key)
.json(&serde_json::json!({
"region": best_region,
"payload": r#"{"players":["p1","p2"]}"#,
}))
.send()
.await?;
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.
- TypeScript / JavaScript
- Go
- Rust
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
results, _ := PingAllRegions()
bestRegion := results[0].Region
body, _ := json.Marshal(map[string]string{
"region": bestRegion,
"payload": `{"players":["p1","p2"]}`,
})
req, _ := http.NewRequest("POST",
fmt.Sprintf("https://api.gameflow.gg/v1/fleets/%s/allocate", gameId),
bytes.NewReader(body),
)
req.Header.Set("X-Api-Key", apiKey)
req.Header.Set("Content-Type", "application/json")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
let results = ping_all_regions().await;
let best_region = &results[0].region;
let client = reqwest::Client::new();
let res = client
.post(format!(
"https://api.gameflow.gg/v1/fleets/{}/allocate",
game_id
))
.header("X-Api-Key", api_key)
.json(&serde_json::json!({
"region": best_region,
"payload": r#"{"players":["p1","p2"]}"#,
}))
.send()
.await?;
See the Custom Game Backend guide for a full integration walkthrough.
Best Practices
- 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 — the initial request includes TLS handshake overhead. Drop it and average the rest for a more accurate measurement.
- 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.