Repository: envoyproxy/gateway

Issue:
Standalone mode Envoy unreasonably slow to become ready / PRE_INITIALIZING takes ~15s-30s
*Description*:

Pretty much what the title says. The [documentation](https://www.envoyproxy.io/docs/envoy/latest/api-v3/admin/v3/server_info.proto#envoy-v3-api-enum-admin-v3-serverinfo-state) says

> PRE_INITIALIZING
⁣Server has not yet completed cluster manager initialization. 


so it looks like it's taking so long to have clusters ready (i guess it's local xds server in eg control plane side)



*Repro Steps*
You can check by running

```
git clone https://github.com/envoyproxy/ai-gateway
cd ai-gateway 
go run ./cmd/aigw run --debug
```

And then you could see the following where it takes 15s AFTER envoy starts to transition from PRE_INITIALIZING to LIVE

```
 starting: /tmp/envoy-gateway/versions/1.35.3/bin/envoy in run directory /tmp/envoy-gateway/runs/1758919585992173000
    xxx_example_test.go:64: error: Get "http://localhost:9901/ready": dial tcp [::1]:9901: connect: connection refused
[2025-09-26 13:46:26.088][170750085][warning][config] [source/server/options_impl_platform_default.cc:9] CPU number provided by HW thread count (instead of cpuset).
    xxx_example_test.go:73: status=503, response: PRE_INITIALIZING
time=2025-09-26T13:46:27.313-07:00 level=INFO msg="Waiting for Envoy to be ready..." status=PRE_INITIALIZING
    xxx_example_test.go:73: status=503, response: PRE_INITIALIZING
    xxx_example_test.go:73: status=503, response: PRE_INITIALIZING
time=2025-09-26T13:46:29.313-07:00 level=INFO msg="Waiting for Envoy to be ready..." status=PRE_INITIALIZING
    xxx_example_test.go:73: status=503, response: PRE_INITIALIZING
    xxx_example_test.go:73: status=503, response: PRE_INITIALIZING
time=2025-09-26T13:46:31.313-07:00 level=INFO msg="Waiting for Envoy to be ready..." status=PRE_INITIALIZING
    xxx_example_test.go:73: status=503, response: PRE_INITIALIZING
    xxx_example_test.go:73: status=503, response: PRE_INITIALIZING
time=2025-09-26T13:46:33.312-07:00 level=INFO msg="Waiting for Envoy to be ready..." status=PRE_INITIALIZING
    xxx_example_test.go:73: status=503, response: PRE_INITIALIZING
    xxx_example_test.go:73: status=503, response: PRE_INITIALIZING
time=2025-09-26T13:46:35.312-07:00 level=INFO msg="Waiting for Envoy to be ready..." status=PRE_INITIALIZING
    xxx_example_test.go:73: status=503, response: PRE_INITIALIZING
    xxx_example_test.go:73: status=503, response: PRE_INITIALIZING
time=2025-09-26T13:46:37.313-07:00 level=INFO msg="Waiting for Envoy to be ready..." status=PRE_INITIALIZING
    xxx_example_test.go:73: status=503, response: PRE_INITIALIZING
    xxx_example_test.go:73: status=503, response: PRE_INITIALIZING
time=2025-09-26T13:46:39.312-07:00 level=INFO msg="Waiting for Envoy to be ready..." status=PRE_INITIALIZING
    xxx_example_test.go:73: status=503, response: PRE_INITIALIZING
    xxx_example_test.go:73: status=503, response: PRE_INITIALIZING
time=2025-09-26T13:46:41.312-07:00 level=INFO msg="Envoy is ready!" status=LIVE
    xxx_example_test.go:73: status=200, response: LIVE
```



Discussion:
@mathetake: cc @codefromthecrypt @nacx @arkodg @shawnh2 @codefromthecrypt: Ack. I did also notice it seems really slow. To the point I needed to put in healthcheck in docker as it takes a very long time.@codefromthecrypt: yeah I reliably get 16.8s I guess we have this 15s overhead@codefromthecrypt: looking at this while at a talk. if I run out of time will pass back

  1. 12:08:26.423 - Config writing starts
  2. 12:08:26.487 - Envoy binary starts
  3. 12:08:41.969 - Route configuration finally accepted (15.5s delay!)
  4. 12:08:43.027 - Ready check passes
@codefromthecrypt: got it. please prioritize merging this ASAP folks! https://github.com/envoyproxy/gateway/pull/7180

Solution patch:
diff --git a/internal/infrastructure/host/proxy_infra.go b/internal/infrastructure/host/proxy_infra.go
index 1dfec8a56..b73057337 100644
--- a/internal/infrastructure/host/proxy_infra.go
+++ b/internal/infrastructure/host/proxy_infra.go
@@ -73,9 +73,11 @@ func (i *Infra) CreateOrUpdateProxyInfra(ctx context.Context, infra *ir.Infra) e
 		XdsServerHost:   ptr.To("0.0.0.0"),
 		AdminServerPort: ptr.To(int32(0)),
 		StatsServerPort: ptr.To(int32(0)),
-	}
-	if i.EnvoyGateway != nil {
-		bootstrapConfigOptions.TopologyInjectorDisabled = i.EnvoyGateway.TopologyInjectorDisabled()
+		// Always disable the topology injector in standalone mode. The topology
+		// injector adds an EDS local_cluster to the bootstrap config for
+		// zone-aware routing, which is both irrelevant outside K8s, but also
+		// causes a 15-30s startup delay on the admin /ready endpoint.
+		TopologyInjectorDisabled: true,
 	}
 	args, err := common.BuildProxyArgs(proxyInfra, proxyConfig.Spec.Shutdown, bootstrapConfigOptions, proxyName, false)
 	if err != nil {



Test patch:
diff --git a/internal/infrastructure/host/proxy_infra_test.go b/internal/infrastructure/host/proxy_infra_test.go
index 3889f5069..aa275fac6 100644
--- a/internal/infrastructure/host/proxy_infra_test.go
+++ b/internal/infrastructure/host/proxy_infra_test.go
@@ -20,9 +20,11 @@ import (
 	egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1"
 	"github.com/envoyproxy/gateway/internal/crypto"
 	"github.com/envoyproxy/gateway/internal/envoygateway/config"
+	"github.com/envoyproxy/gateway/internal/infrastructure/common"
 	"github.com/envoyproxy/gateway/internal/ir"
 	"github.com/envoyproxy/gateway/internal/logging"
 	"github.com/envoyproxy/gateway/internal/utils/file"
+	"github.com/envoyproxy/gateway/internal/xds/bootstrap"
 	"github.com/envoyproxy/gateway/test/utils"
 )
 
@@ -272,3 +274,74 @@ func TestGetEnvoyVersion(t *testing.T) {
 		})
 	}
 }
+
+// TestTopologyInjectorDisabledInHostMode verifies we don't cause a 15+ second
+// startup delay in standalone mode as Envoy waits for endpoint discovery.
+// See: https://github.com/envoyproxy/gateway/issues/7080
+func TestTopologyInjectorDisabledInHostMode(t *testing.T) {
+	testCases := []struct {
+		name                          string
+		topologyInjectorDisabled      bool
+		expectLocalClusterInBootstrap bool
+	}{
+		{
+			name:                          "topology injector enabled",
+			topologyInjectorDisabled:      false,
+			expectLocalClusterInBootstrap: true,
+		},
+		{
+			name:                          "topology injector disabled",
+			topologyInjectorDisabled:      true,
+			expectLocalClusterInBootstrap: false,
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			proxyInfra := &ir.ProxyInfra{
+				Name:      "test-proxy",
+				Namespace: "default",
+				Config: &egv1a1.EnvoyProxy{
+					Spec: egv1a1.EnvoyProxySpec{
+						Logging: egv1a1.ProxyLogging{
+							Level: map[egv1a1.ProxyLogComponent]egv1a1.LogLevel{
+								egv1a1.LogComponentDefault: egv1a1.LogLevelInfo,
+							},
+						},
+					},
+				},
+			}
+
+			bootstrapConfigOptions := &bootstrap.RenderBootstrapConfigOptions{
+				ProxyMetrics: &egv1a1.ProxyMetrics{
+					Prometheus: &egv1a1.ProxyPrometheusProvider{
+						Disable: true,
+					},
+				},
+				XdsServerHost:            ptr.To("0.0.0.0"),
+				AdminServerPort:          ptr.To(int32(0)),
+				StatsServerPort:          ptr.To(int32(0)),
+				TopologyInjectorDisabled: tc.topologyInjectorDisabled,
+			}
+
+			args, err := common.BuildProxyArgs(proxyInfra, nil, bootstrapConfigOptions, "test-node", false)
+			require.NoError(t, err)
+
+			// Extract the bootstrap YAML from args (it's after --config-yaml)
+			var bootstrapYAML string
+			for i, arg := range args {
+				if arg == "--config-yaml" && i+1 < len(args) {
+					bootstrapYAML = args[i+1]
+					break
+				}
+			}
+			require.NotEmpty(t, bootstrapYAML, "bootstrap YAML not found in args")
+
+			if tc.expectLocalClusterInBootstrap {
+				require.Contains(t, bootstrapYAML, "local_cluster_name:")
+			} else {
+				require.NotContains(t, bootstrapYAML, "local_cluster_name:")
+			}
+		})
+	}
+}