diff --git a/cs/src/Contracts/ClusterAvailability.cs b/cs/src/Contracts/ClusterAvailability.cs
new file mode 100644
index 00000000..ee993a85
--- /dev/null
+++ b/cs/src/Contracts/ClusterAvailability.cs
@@ -0,0 +1,27 @@
+//
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license.
+//
+
+namespace Microsoft.DevTunnels.Contracts;
+
+///
+/// Availability status of a tunneling service cluster.
+///
+public enum ClusterAvailability
+{
+ ///
+ /// Cluster has sufficient capacity and is fully available.
+ ///
+ Available,
+
+ ///
+ /// Cluster is approaching capacity limits and may experience delays.
+ ///
+ Degraded,
+
+ ///
+ /// Cluster is at or beyond capacity and should not be used for new tunnels.
+ ///
+ Unavailable,
+}
diff --git a/cs/src/Contracts/ClusterRecommendation.cs b/cs/src/Contracts/ClusterRecommendation.cs
new file mode 100644
index 00000000..965fbe6a
--- /dev/null
+++ b/cs/src/Contracts/ClusterRecommendation.cs
@@ -0,0 +1,47 @@
+//
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license.
+//
+
+namespace Microsoft.DevTunnels.Contracts;
+
+///
+/// A single cluster recommendation with availability and capacity details.
+///
+public class ClusterRecommendation
+{
+ ///
+ /// Gets or sets the cluster ID, e.g. "usw2".
+ ///
+ public string ClusterId { get; set; } = null!;
+
+ ///
+ /// Gets or sets the Azure location name, e.g. "WestUs2".
+ ///
+ public string AzureLocation { get; set; } = null!;
+
+ ///
+ /// Gets or sets the Azure geography name for data residency, e.g. "United States".
+ ///
+ public string AzureGeo { get; set; } = null!;
+
+ ///
+ /// Gets or sets the cluster URI for API requests.
+ ///
+ public string ClusterUri { get; set; } = null!;
+
+ ///
+ /// Gets or sets the availability status of the cluster.
+ ///
+ public ClusterAvailability Availability { get; set; }
+
+ ///
+ /// Gets or sets the utilization percentage of the cluster.
+ ///
+ public double UtilizationPercent { get; set; }
+
+ ///
+ /// Gets or sets a human-readable reason for this recommendation's ranking.
+ ///
+ public string Reason { get; set; } = null!;
+}
diff --git a/cs/src/Contracts/ClusterRecommendationResponse.cs b/cs/src/Contracts/ClusterRecommendationResponse.cs
new file mode 100644
index 00000000..2becfa42
--- /dev/null
+++ b/cs/src/Contracts/ClusterRecommendationResponse.cs
@@ -0,0 +1,37 @@
+//
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license.
+//
+
+using System;
+
+namespace Microsoft.DevTunnels.Contracts;
+
+///
+/// Response from the cluster recommendation API containing ranked cluster recommendations.
+///
+public class ClusterRecommendationResponse
+{
+ ///
+ /// Gets or sets the preferred cluster ID that was requested, if any.
+ ///
+ public string? PreferredClusterId { get; set; }
+
+ ///
+ /// Gets or sets the recommended cluster ID — the best available cluster.
+ /// Null if no clusters are available.
+ ///
+ public string? RecommendedClusterId { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether the recommendation differs
+ /// from the preferred cluster.
+ ///
+ public bool IsFallback { get; set; }
+
+ ///
+ /// Gets or sets the ordered list of cluster recommendations, ranked by preference.
+ ///
+ public ClusterRecommendation[] Recommendations { get; set; }
+ = Array.Empty();
+}
diff --git a/cs/src/Management/ITunnelManagementClient.cs b/cs/src/Management/ITunnelManagementClient.cs
index 6bfc6b3f..5410a4f1 100644
--- a/cs/src/Management/ITunnelManagementClient.cs
+++ b/cs/src/Management/ITunnelManagementClient.cs
@@ -395,6 +395,25 @@ Task ResolveSubjectsAsync(
/// Array of
Task ListClustersAsync(CancellationToken cancellation = default);
+ ///
+ /// Gets cluster recommendations for tunnel creation based on capacity and
+ /// availability.
+ ///
+ ///
+ /// Optional preferred cluster ID. When omitted, defaults to the cluster
+ /// serving the request.
+ ///
+ ///
+ /// Optional Azure geography filter. When specified, only clusters in
+ /// this geo are eligible for recommendation.
+ ///
+ /// Cancellation token.
+ /// Cluster recommendation response with ranked clusters.
+ Task GetClusterRecommendationsAsync(
+ string? preferredClusterId = null,
+ string? requiredGeo = null,
+ CancellationToken cancellation = default);
+
///
/// Checks for tunnel name availability.
///
diff --git a/cs/src/Management/TunnelManagementClient.cs b/cs/src/Management/TunnelManagementClient.cs
index 47ce73e5..8ce2694f 100644
--- a/cs/src/Management/TunnelManagementClient.cs
+++ b/cs/src/Management/TunnelManagementClient.cs
@@ -42,6 +42,7 @@ public class TunnelManagementClient : ITunnelManagementClient
private const string EventsApiSubPath = "/events";
private const string ClustersApiPath = "/clusters";
private const string ClustersV1ApiPath = ApiV1Path + "/clusters";
+ private const string RecommendationsSubPath = "/recommendations";
private const string TunnelAuthenticationScheme = "Tunnel";
private const string RequestIdHeaderName = "VsSaaS-Request-Id";
private const string CheckAvailableSubPath = ":checkNameAvailability";
@@ -1084,6 +1085,29 @@ public async Task CreateTunnelAsync(
{
Requires.NotNull(tunnel, nameof(tunnel));
options ??= new TunnelRequestOptions();
+
+ // If the caller didn't specify a cluster, auto-select one via the
+ // recommendations API. Failures fall back to global routing.
+ if (string.IsNullOrEmpty(tunnel.ClusterId))
+ {
+ try
+ {
+ var recommendations = await GetClusterRecommendationsAsync(
+ preferredClusterId: null,
+ requiredGeo: options.RequiredGeo,
+ cancellation);
+ if (!string.IsNullOrEmpty(recommendations?.RecommendedClusterId))
+ {
+ tunnel.ClusterId = recommendations!.RecommendedClusterId;
+ }
+ }
+ catch (Exception) when (!cancellation.IsCancellationRequested)
+ {
+ // Fall through to global (Traffic Manager) routing if the
+ // recommendations request fails for any reason.
+ }
+ }
+
options.AdditionalHeaders ??= new List>();
options.AdditionalHeaders = options.AdditionalHeaders.Append(
new KeyValuePair("If-None-Match", "*"));
@@ -1595,6 +1619,46 @@ public async Task ListClustersAsync(CancellationToken cancella
return clusterDetails!;
}
+ ///
+ public async Task GetClusterRecommendationsAsync(
+ string? preferredClusterId = null,
+ string? requiredGeo = null,
+ CancellationToken cancellation = default)
+ {
+ var baseAddress = this.httpClient.BaseAddress!;
+ var builder = new UriBuilder(baseAddress);
+ builder.Path = ClustersPath + RecommendationsSubPath;
+
+ var queryParts = new List();
+ var apiQuery = GetApiQuery();
+ if (!string.IsNullOrEmpty(apiQuery))
+ {
+ queryParts.Add(apiQuery!);
+ }
+
+ if (!string.IsNullOrEmpty(preferredClusterId))
+ {
+ queryParts.Add(
+ $"preferredClusterId={Uri.EscapeDataString(preferredClusterId!)}");
+ }
+
+ if (!string.IsNullOrEmpty(requiredGeo))
+ {
+ queryParts.Add($"requiredGeo={Uri.EscapeDataString(requiredGeo!)}");
+ }
+
+ builder.Query = string.Join("&", queryParts);
+
+ var response = await SendRequestAsync