Unity Netcode + Relay: UnityTransport.ProcessEvent → “Failed to connect to server” even though Relay join REST call succeeds

I am encountering reproducible Relay connection failure when using Netcode for GameObjects + Unity Transport (UTP).

Netcode Relay is not working. The error appears during the transport early update phase:

Failed to connect to server.
UnityEngine.Debug:LogError (object)
Unity.Netcode.Transports.UTP.UnityTransport:ProcessEvent () (at ./Library/PackageCache/com.unity.netcode.gameobjects@bd2a018756ed/Runtime/Transports/UTP/UnityTransport.cs:1010)
Unity.Netcode.Transports.UTP.UnityTransport:OnEarlyUpdate () (at ./Library/PackageCache/com.unity.netcode.gameobjects@bd2a018756ed/Runtime/Transports/UTP/UnityTransport.cs:1063)
Unity.Netcode.NetworkTransport:EarlyUpdate () (at ./Library/PackageCache/com.unity.netcode.gameobjects@bd2a018756ed/Runtime/Transports/NetworkTransport.cs:131)
Unity.Netcode.NetworkManager:NetworkUpdate (Unity.Netcode.NetworkUpdateStage) (at ./Library/PackageCache/com.unity.netcode.gameobjects@bd2a018756ed/Runtime/Core/NetworkManager.cs:341)
Unity.Netcode.NetworkUpdateLoop:RunNetworkUpdateStage (Unity.Netcode.NetworkUpdateStage) (at ./Library/PackageCache/com.unity.netcode.gameobjects@bd2a018756ed/Runtime/Core/NetworkUpdateLoop.cs:191)
Unity.Netcode.NetworkUpdateLoop/NetworkEarlyUpdate/<>c:<CreateLoopSystem>b__0_0 () (at ./Library/PackageCache/com.unity.netcode.gameobjects@bd2a018756ed/Runtime/Core/NetworkUpdateLoop.cs:214)

This happens after successfully:

  • Initializing Unity Services
  • Signing in anonymously
  • Joining a Relay allocation using a valid join code
  • Calling SetRelayServerData
  • Calling NetworkManager.StartClient()

No exceptions are thrown during Relay join allocation or client start.


Environment

  • Unity version: (please assume recent LTS if relevant)
  • Transport: com.unity.transport @ 2.6.0
  • Relay: Unity Relay Service
  • Platform: Android (also tested in Editor)
  • Connection type: UDP

NOTE: All of the following code execute successfully. No error.

Client Join Code (Context)

public async Task JoinAsync(string joinCode)
{
    await EnsureServicesAsync();

    if (_networkManager.IsListening)
        _networkManager.Shutdown();

    var allocation = await RelayService.Instance.JoinAllocationAsync(joinCode);
    var serverData = AllocationUtils.ToRelayServerData(allocation, "udp");

    _unityTransport.SetRelayServerData(serverData);

    // Yield one frame before starting client
    await Task.Yield();

    _networkManager.StartClient();
}

Authentication is guaranteed before joining:

private async Task EnsureServicesAsync()
{
    if (UnityServices.State == ServicesInitializationState.Uninitialized)
        await UnityServices.InitializeAsync();

    if (!AuthenticationService.Instance.IsSignedIn)
        await AuthenticationService.Instance.SignInAnonymouslyAsync();

    while (!AuthenticationService.Instance.IsSignedIn)
        await Task.Yield();
}

Additional Context: Successful Raw Relay Join Request

Note: The presence of a non-empty hostConnectionData value (for example, "TlwhkVL8geh1ObNysgmVnshr0IKKOx4+xyEEv/3mGia+nyUeqXqRnNWkC0Lm9LY7+cU=") indicates that a host has successfully connected to the Relay allocation. This confirms that the headless Netcode host is actively running and that the Relay server is functioning correctly, as this field is only populated once the host is established and sending heartbeats.

To rule out Relay service or credential issues, I tested the same join code using a raw REST request.

The request consistently succeeds and returns a valid allocation with endpoints.

Request

curl --location 'https://relay-allocations.services.api.unity.com/v1/join' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer <player_access_token>' \
--data '{
    "joinCode": "9899P6"
}'

Response (200 OK)

  • Valid allocationId
  • Valid connectionData, hostConnectionData, and key
  • Valid Relay server endpoints:
udp : 34.158.45.226:37000
dtls : 34.158.45.226:37001
wss : *.relay.cloud.unity3d.com:37011

This suggests:

  • Join code is valid
  • Token is valid
  • Allocation exists and is reachable
  • Relay service itself is functioning correctly
  • The presence of a non-empty hostConnectionData value (for example, "TlwhkVL8geh1ObNysgmVnshr0IKKOx4+xyEEv/3mGia+nyUeqXqRnNWkC0Lm9LY7+cU=") indicates Netcode host is actively running and that the Relay server is functioning correctly, as this field is only populated once the host is established and sending heartbeats.
curl --location 'https://relay-allocations.services.api.unity.com/v1/join' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5...' \
--data '{
    "joinCode": "9899P6"
}'

RESPONSE:
{
    "data": {
        "allocation": {
            "allocationId": "6c9a0c4e-7400-4a78-8ebb-07fea8a2b73c",
            "allocationIdBytes": "bJoMTnQASniOuwf+qKK3PA==",
            "connectionData": "zmV8e6DVTHBG3Bvw2wHewJxznbPSoa6ml/n5B8OovzHqgqRBsigCkVf/rRuCqpJ3854AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
            "hostConnectionData": "TlwhkVL8geh1ObNysgmVnshr0IKKOx4+xyEEv/3mGia+nyUeqXqRnNWkC0Lm9LY7+cU=",
            "key": "/O5JRiInXu78ZGKIFhSI5POMAYc6xvP5zmW3rn+RfD9daTDofT6cOf9xkFUJy1szCaByYdS3e0dWbmRylG3WkQ==",
            "region": "asia-southeast1",
            "relayServer": {
                "ipV4": "34.158.45.226",
                "port": 37000
            },
            "serverEndpoints": [
                {
                    "connectionType": "udp",
                    "host": "34.158.45.226",
                    "network": "udp",
                    "port": 37000,
                    "reliable": false,
                    "secure": false
                },
                {
                    "connectionType": "dtls",
                    "host": "34.158.45.226",
                    "network": "udp",
                    "port": 37001,
                    "reliable": false,
                    "secure": true
                },
                {
                    "connectionType": "wss",
                    "host": "6790645139760809820-asia-southeast1.relay.cloud.unity3d.com",
                    "network": "tcp",
                    "port": 37011,
                    "reliable": true,
                    "secure": true
                }
            ]
        }
    },
    "meta": {
        "requestId": "65d3649d-60b0-496d-89bc-224dc68be6d4",
        "status": 200
    }
}

Hi @Felix-Unity, sorry to bother you again. Do you have any insight into this issue? Thank you.

Could you share your logic for creating the Relay Allocation from the Host perspective?

Hi @Felix-Unity ,

The Relay allocation itself is created using the Unity Relay REST API, for example:

curl --location 'https://relay-allocations.services.api.unity.com/v1/allocate' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6I...' \
--data '{
"maxConnections": 10,
"region": "us-east1"
}
'

RESPONSE:

{
    "data": {
        "allocation": {
            "allocationId": "c68f1dfb-e27f-49e4-b2b3-17d68a203df8",
            "allocationIdBytes": "xo8d++J/SeSysxfWiiA9+A==",
            "connectionData": "990t//8K/BMshguupgNPPezo6W5/7oKth6sDssrCPvrynd8CnS3/UZiguPhKgpYJhsUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
            "key": "67WUzgtO3fglBbGOas6hU/alSvcS288bGFj7hYTnjWPiqPswmup6ME0bgJna0FPDJf8FPGv76KKwLoKNI1z1SA==",
            "region": "us-east1",
            "relayServer": {
                "ipV4": "34.23.13.136",
                "port": 37000
            },
            "serverEndpoints": [
                {
                    "connectionType": "udp",
                    "host": "34.23.13.136",
                    "network": "udp",
                    "port": 37000,
                    "reliable": false,
                    "secure": false
                },
                {
                    "connectionType": "dtls",
                    "host": "34.23.13.136",
                    "network": "udp",
                    "port": 37001,
                    "reliable": false,
                    "secure": true
                },
                {
                    "connectionType": "wss",
                    "host": "822044081963642814-us-east1.relay.cloud.unity3d.com",
                    "network": "tcp",
                    "port": 37011,
                    "reliable": true,
                    "secure": true
                }
            ]
        }
    },
    "meta": {
        "requestId": "9af750ef-8a6a-429a-b589-b1c4b0ea1b10",
        "status": 201
    }
}

Below is the code we use for hosting a Relay allocation from the Host & Server perspective.

The method HandleHostRoomRequestAsync handles incoming HTTP requests to host relay alloc, calls relayBootstrap.HostWithRelayAsync with the allocation and connection data.

All of the code executes successfully and keeps the relay allocation alive. Thank you.

private async Task HandleHostRoomRequestAsync(HttpListenerContext context, CancellationToken token)
{
    var response = context.Response;
    response.ContentType = "application/json";
    response.ContentEncoding = Encoding.UTF8;

    try
    {
        using var reader = new System.IO.StreamReader(context.Request.InputStream, Encoding.UTF8);
        var body = await reader.ReadToEndAsync();

        var payload = JsonConvert.DeserializeObject<HostRoomRequest>(body);

        var hostConnectionData = payload.HostConnectionData.Length == 0 ? payload.ConnectionData : payload.HostConnectionData;

        await relayBootstrap.HostWithRelayAsync(
            payload.JoinCode,
            payload.AllocationId,
            payload.AllocationIdBytes,
            payload.Key,
            payload.ConnectionData,
            hostConnectionData,
            payload.IpV4Address,
            payload.Port,
            payload.Protocol);

        response.StatusCode = (int)HttpStatusCode.OK;

    }
    catch (Exception ex)
    {
        //
    }
}

public async Task HostWithRelayAsync(
    string joinCode,
    string allocationId,
    byte[] allocationIdBytes,
    byte[] key,
    byte[] connectionData,
    byte[] hostConnectionData,
    string ipv4Address,
    ushort port,
    string protocol)
{
    try
    {
        await EnsureServicesAsync();

        await RunOnUnityThread(async () =>
        {
            var utp = GetUnityTransportOrError();

            var serverData = new RelayServerData(
                ipv4Address,
                port,
                Guid.Parse(allocationId).ToByteArray(),
                connectionData,
                hostConnectionData,
                key,
                false
            );

            var nm = NetworkManager.Singleton;
            if (nm.IsHost || nm.IsServer || nm.IsListening)
            {
                nm.Shutdown();
            }

            // Also tried this approach, it's working too.
            //var allocation = await RelayService.Instance.JoinAllocationAsync(joinCode);
            //if (allocation.AllocationId == Guid.Empty)
            //    throw new Exception("Invalid allocation received from Relay Service.");
            //var serverData = AllocationUtils.ToRelayServerData(allocation, "udp");

            utp.SetRelayServerData(serverData);
            nm.StartHost();
        });
    }
    catch (Exception ex)
    {
        //
    }
}

private static SynchronizationContext _unityContext;

private static Task RunOnUnityThread(Func<Task> work)
{
    if (_unityContext == null)
    {
        throw new InvalidOperationException(
            "[ServerLobbyBootstrap] Unity SynchronizationContext not initialized. " +
            "Ensure this component is created on the Unity main thread.");
    }

    var tcs = new TaskCompletionSource<bool>();

    _unityContext.Post(async _ =>
    {
        try
        {
            await work();
            tcs.SetResult(true);
        }
        catch (Exception ex)
        {
            tcs.SetException(ex);
        }
    }, null);

    return tcs.Task;
}

Are you able to capture the traffic on the client using a tool like Wireshark?

Also what version of the Unity Transport package are you using?

Hi @simon-lemay-unity,

The Unity Transport package version in use is com.unity.transport @ 2.6.0.

Yes, I was able to capture traffic on the client using Wireshark. When the following code executes:

var allocation = await RelayService.Instance.JoinAllocationAsync(joinCode);

var serverData = AllocationUtils.ToRelayServerData(allocation, "udp");

_unityTransport.SetRelayServerData(serverData);

_networkManager.StartClient();

these are the initial packets seen on the client in Wireshark:

74	5.396633	2404:6800:4017:804::200a	2001:4450:4f48:d300:d508:6b9b:bcac:488	UDP	87	443 → 59958 Len=25
107	8.857519	2001:4450:4f48:d300:d508:6b9b:bcac:488	2405:b800:fef:ff00::5	DNS	120	Standard query 0x60b1 AAAA relay-allocations.services.api.unity.com
108	8.897281	2001:4450:4f48:d300:d508:6b9b:bcac:488	2405:b800:fef:ff00::4	DNS	120	Standard query 0x60b1 AAAA relay-allocations.services.api.unity.com
109	8.936585	2405:b800:fef:ff00::4	2001:4450:4f48:d300:d508:6b9b:bcac:488	DNS	187	Standard query response 0x60b1 AAAA relay-allocations.services.api.unity.com SOA use4.akam.net
115	9.013602	2405:b800:fef:ff00::5	2001:4450:4f48:d300:d508:6b9b:bcac:488	DNS	187	Standard query response 0x60b1 AAAA relay-allocations.services.api.unity.com SOA use4.akam.net
152	9.641903	192.168.1.4	34.124.198.192	DTLSv1.2	522	Client Hello (SNI=34.124.198.192)
154	9.673997	34.124.198.192	192.168.1.4	DTLSv1.2	90	Hello Verify Request
155	9.679060	192.168.1.4	34.124.198.192	DTLSv1.2	542	Client Hello (SNI=34.124.198.192)
156	9.710984	34.124.198.192	192.168.1.4	DTLSv1.2	136	Server Hello, Server Hello Done
157	9.712147	192.168.1.4	34.124.198.192	DTLSv1.2	85	Client Key Exchange
158	9.712160	192.168.1.4	34.124.198.192	DTLSv1.2	56	Change Cipher Spec
159	9.712165	192.168.1.4	34.124.198.192	DTLSv1.2	103	Encrypted Handshake Message
160	9.742845	34.124.198.192	192.168.1.4	DTLSv1.2	117	Change Cipher Spec, Encrypted Handshake Message
166	10.623534	192.168.1.4	34.124.198.192	DTLSv1.2	374	Application Data
167	10.654255	34.124.198.192	192.168.1.4	DTLSv1.2	83	Application Data
168	10.660200	192.168.1.4	34.124.198.192	DTLSv1.2	355	Application Data
169	10.671371	2404:6800:4017:804::200a	2001:4450:4f48:d300:d508:6b9b:bcac:488	UDP	140	443 → 59958 Len=78
170	10.684665	2001:4450:4f48:d300:d508:6b9b:bcac:488	2404:6800:4017:804::200a	UDP	95	59958 → 443 Len=33
171	10.690765	34.124.198.192	192.168.1.4	DTLSv1.2	115	Application Data
172	10.696532	192.168.1.4	34.124.198.192	DTLSv1.2	130	Application Data
178	11.201113	192.168.1.4	34.124.198.192	DTLSv1.2	101	Application Data

That’s weird. The packet capture is showing a (successful) DTLS handshake but you’re building server data for the unencrypted UDP protocol variant. Can you try switching that "udp" string to "dtls"?

Another thing you could try is building the RelayServerData object manually (like you’re doing on the server). All of the required fields should be available from the JoinAllocation object.

Hi @simon-lemay-unity ,

Client: Tried this code with UDP and it executes successfully:

var allocation = await RelayService.Instance.JoinAllocationAsync(joinCode);
RelayServerEndpoint relayServer = null;
foreach (RelayServerEndpoint endPoint in allocation.ServerEndpoints)
{
    if (endPoint.ConnectionType == "udp")
    {
        relayServer = endPoint;
    }
}

_unityTransport.SetRelayServerData(
    relayServer.Host,
    (ushort)relayServer.Port,
    allocation.AllocationIdBytes,
    allocation.Key,
    allocation.ConnectionData,
    allocation.HostConnectionData
);

_networkManager.StartClient();

After a minute, same error:

Failed to connect to server.Failed to connect to server.
UnityEngine.Debug:LogError (object)
Unity.Netcode.Transports.UTP.UnityTransport:ProcessEvent ()

Wireshark packets seen:

23  2.540961    2001:4450:4f48:d300:e109:a7f2:ee42:419f 2405:b800:fef:ff00::5   DNS 120 Standard query 0xa092 AAAA relay-allocations.services.api.unity.com
24  2.566634    2405:b800:fef:ff00::5   2001:4450:4f48:d300:e109:a7f2:ee42:419f DNS 187 Standard query response 0xa092 AAAA relay-allocations.services.api.unity.com SOA use4.akam.net
28  2.571408    192.168.1.4 34.160.10.162   TLSv1.2 437 Client Hello (SNI=relay-allocations.services.api.unity.com)
49  3.005035    192.168.1.4 34.124.198.192  UDP 64  58205 → 37000 Len=22
54  3.085018    192.168.1.4 34.124.198.192  UDP 337 58205 → 37000 Len=295
56  3.160606    34.124.198.192  192.168.1.4 UDP 60  37000 → 58205 Len=4
57  3.228041    192.168.1.4 34.124.198.192  UDP 337 59263 → 37000 Len=295
58  3.301179    34.124.198.192  192.168.1.4 UDP 60  37000 → 59263 Len=4
59  3.301436    192.168.1.4 34.124.198.192  UDP 318 59263 → 37000 Len=276
60  3.374636    34.124.198.192  192.168.1.4 UDP 78  37000 → 59263 Len=36




Client: Also tried this code with DTLS and it executes successfully:

var allocation = await RelayService.Instance.JoinAllocationAsync(joinCode);
RelayServerEndpoint relayServer = null;
foreach (RelayServerEndpoint endPoint in allocation.ServerEndpoints)
{
    if (endPoint.ConnectionType == "dtls")
    {
        relayServer = endPoint;
    }
}

_unityTransport.SetRelayServerData(
    relayServer.Host,
    (ushort)relayServer.Port,
    allocation.AllocationIdBytes,
    allocation.Key,
    allocation.ConnectionData,
    allocation.HostConnectionData,
    true
);

_networkManager.StartClient();

After a minute, same error:

Failed to connect to server.Failed to connect to server.
UnityEngine.Debug:LogError (object)
Unity.Netcode.Transports.UTP.UnityTransport:ProcessEvent ()

Wireshark (DTLS) packets:

17	2.012715	2001:4450:4f48:d300:e109:a7f2:ee42:419f	2405:b800:fef:ff00::5	DNS	120	Standard query 0xff87 AAAA relay-allocations.services.api.unity.com
18	2.015790	192.168.1.4	34.124.198.192	UDP	64	49302 → 37000 Len=22
19	2.035686	2405:b800:fef:ff00::5	2001:4450:4f48:d300:e109:a7f2:ee42:419f	DNS	187	Standard query response 0xff87 AAAA relay-allocations.services.api.unity.com SOA use4.akam.net
23	2.039741	192.168.1.4	34.160.10.162	TLSv1.2	437	Client Hello (SNI=relay-allocations.services.api.unity.com)
49	2.515817	192.168.1.4	34.124.198.192	UDP	64	49302 → 37000 Len=22
57	2.736123	192.168.1.4	34.124.198.192	DTLSv1.2	522	Client Hello (SNI=34.124.198.192)
58	2.810388	34.124.198.192	192.168.1.4	DTLSv1.2	90	Hello Verify Request
59	2.818008	192.168.1.4	34.124.198.192	DTLSv1.2	542	Client Hello (SNI=34.124.198.192)
60	2.891870	34.124.198.192	192.168.1.4	DTLSv1.2	136	Server Hello, Server Hello Done
61	2.892844	192.168.1.4	34.124.198.192	DTLSv1.2	85	Client Key Exchange
62	2.892860	192.168.1.4	34.124.198.192	DTLSv1.2	56	Change Cipher Spec
63	2.892876	192.168.1.4	34.124.198.192	DTLSv1.2	103	Encrypted Handshake Message
64	2.965822	34.124.198.192	192.168.1.4	DTLSv1.2	117	Change Cipher Spec, Encrypted Handshake Message
65	3.015796	192.168.1.4	34.124.198.192	UDP	64	49302 → 37000 Len=22
66	3.115800	192.168.1.4	34.124.198.192	UDP	337	49302 → 37000 Len=295
67	3.200044	34.124.198.192	192.168.1.4	UDP	60	37000 → 49302 Len=4
68	3.615798	192.168.1.4	34.124.198.192	UDP	64	49302 → 37000 Len=22
69	3.710183	192.168.1.4	34.124.198.192	DTLSv1.2	374	Application Data
70	3.783094	34.124.198.192	192.168.1.4	DTLSv1.2	83	Application Data
71	3.783506	192.168.1.4	34.124.198.192	DTLSv1.2	355	Application Data
72	3.856324	34.124.198.192	192.168.1.4	DTLSv1.2	115	Application Data
73	3.857906	192.168.1.4	34.124.198.192	DTLSv1.2	130	Application Data
74	3.939430	34.124.198.192	192.168.1.4	UDP	93	37000 → 49302 Len=51
75	4.115788	192.168.1.4	34.124.198.192	UDP	64	49302 → 37000 Len=22

Could you share the full capture (.pcap) file, including up to the moment when the connection failure error is logged? I’m curious to know exactly what’s going on in those exchanges since at a glance it doesn’t look like a typical exchange with the Relay server.

Also just to confirm: there’s no other error in the logs aside from that “failed to connect” message, right?

Yes, that is the only error in the logs:

Failed to connect to server.
UnityEngine.Debug:LogError (object)
Unity.Netcode.Transports.UTP.UnityTransport:ProcessEvent ()

Here are the full packet captures from _networkManager.StartClient() until the Failed to connect to server. error is logged.

UDP capture:

DTLS capture:

Thanks for that! You’re running both host and client on the same machine, right?
(Nothing wrong with that to be clear. Just want to confirm what I’m seeing in the captures.)

From what I can see, there appears to be two problems going on:

  • In the UDP capture, the client appears to be attempting to connect to the Relay server using DTLS instead of clear UDP, but it’s doing so on the port the UDP server is expecting connections on. It’s like the server data is being misconfigured.
  • In the DTLS capture, the connection is established successfully with the Relay, but then when the client attempts to handshake with the host, the host doesn’t respond. That one is more puzzling, I’m not sure what could be causing this.

For the first problem, could you verify the value of the IsSecure field of the RelayServerData object right before you pass it to SetRelayServerData? It should be 0 when using the "udp" connection type. If it’s not then there’s something very wrong in the way we construct that object.

Another thing I’d verify is if you are actually calling that Shutdown method on you NetworkManager on the host side. More precisely, I’m wondering if that bit of code could be causing problems:

var nm = NetworkManager.Singleton;
if (nm.IsHost || nm.IsServer || nm.IsListening)
{
    nm.Shutdown();
}

utp.SetRelayServerData(serverData);
nm.StartHost();

Normally NGO requires a bit of time to effect a complete shutdown, so I’m wondering if perhaps shutting it down and then immediately starting it again might be messing with some things on the host side…

Also is the issue reproducible in a simple reproduction project that you could share? Or would you be able to share your project that we can have a look? (Feel free to reach out privately for this.)

Thanks for the help, the insights helped a lot.

The overall approach should work. The issue turned out to be on my side. I initially made a mistake by using separate .cs scripts for the NetworkBehaviour on the client and server side. That led to inconsistent behavior and random errors during the Relay handshake and host communication.

After aligning to the recommended approach, using a single shared NetworkBehaviour script for both client and server, the issues stopped occurring. The Relay configuration and connection flow now behave as expected.