Repository: envoyproxy/gateway

Issue:
Routes with multiple parent references only update status for one parent (v1.5.1 regression)
## Bug Report

### Description
When an HTTPRoute (or any route) has multiple parent references pointing to different gateways managed by the same Envoy Gateway controller, only one parent reference gets its status updated in v1.5.1. This is a regression from v1.5.0 where all parent references were properly updated.

### Impact
This breaks external-dns and other tools that rely on route status to determine which routes are accepted by which gateways.

### Root Cause
The bug was introduced in commit 44f43eae2 (#6812) which attempted to cleanup dangling route status conditions. The new `mergeRouteParentStatus` function incorrectly removes parent statuses when:
- The parent is managed by our controller 
- The parent is not in the new status update

This happens because the function assumes that if a parent managed by our controller isn't in the new status, it should be removed. However, when processing routes with multiple parent references, each reconciliation may only update specific parents, not all of them.

### Reproduction Steps
1. Create an HTTPRoute with multiple parent references to different gateways managed by the same controller:
```yaml
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: example
spec:
  parentRefs:
    - name: gateway-1
      namespace: default
    - name: gateway-2
      namespace: default
  hostnames:
    - example.com
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /
      backendRefs:
        - name: example-service
          port: 80
```

2. Check the route status: `kubectl get httproute example -o jsonpath='{.status.parents}'`
3. Only one parent will have a status entry instead of both

### Expected Behavior
All parent references should have their status properly updated and maintained.

### Actual Behavior 
Only the last processed parent reference retains its status; others are incorrectly removed during the merge.

### Environment
- Envoy Gateway Version: v1.5.1
- Kubernetes Version: Any
- Gateway API Version: v1.0+

### Workaround
1. Downgrade to v1.5.0
2. Or split routes with multiple parent references into separate routes (one per gateway)

### Proposed Fix
The `mergeRouteParentStatus` function needs to be corrected to properly handle multiple parent references from the same controller. The logic should not remove parent statuses just because they're not in the current update batch.

Hints from issue comments:

Solution patch:
diff --git a/.github/workflows/build_and_test.yaml b/.github/workflows/build_and_test.yaml
index 0cf2128d4..4943cf217 100644
--- a/.github/workflows/build_and_test.yaml
+++ b/.github/workflows/build_and_test.yaml
@@ -252,8 +252,6 @@ jobs:
     - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8  # v5.0.0
     - uses: ./tools/github-actions/setup-deps
 
-    - name: Setup Graphviz
-      uses: ts-graphviz/setup-graphviz@b1de5da23ed0a6d14e0aeee8ed52fdd87af2363c  # v2.0.2
 
     # Benchmark
     - name: Run Benchmark tests
@@ -266,6 +264,7 @@ jobs:
         BENCHMARK_CPU_LIMITS: 1000m
         BENCHMARK_MEMORY_LIMITS: 2000Mi
         BENCHMARK_REPORT_DIR: benchmark_report
+        BENCHMARK_RENDER_PNG: "false"
       run: make benchmark
 
     - name: Upload Benchmark report
diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
index d892348fc..1b06e1c85 100644
--- a/.github/workflows/release.yaml
+++ b/.github/workflows/release.yaml
@@ -18,8 +18,6 @@ jobs:
       - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8  # v5.0.0
       - uses: ./tools/github-actions/setup-deps
 
-      - name: Setup Graphviz
-        uses: ts-graphviz/setup-graphviz@b1de5da23ed0a6d14e0aeee8ed52fdd87af2363c  # v2.0.2
 
       # Benchmark
       - name: Run Benchmark tests
@@ -33,6 +31,7 @@ jobs:
           BENCHMARK_CPU_LIMITS: 1000m
           BENCHMARK_MEMORY_LIMITS: 2000Mi
           BENCHMARK_REPORT_DIR: benchmark_report
+          BENCHMARK_RENDER_PNG: "false"
         run: make benchmark
 
       - name: Package benchmark report
diff --git a/internal/provider/kubernetes/status.go b/internal/provider/kubernetes/status.go
index 5735260e0..a0e4f0ef7 100644
--- a/internal/provider/kubernetes/status.go
+++ b/internal/provider/kubernetes/status.go
@@ -109,7 +109,7 @@ func (r *gatewayAPIReconciler) subscribeAndUpdateStatus(ctx context.Context, ext
 							Spec:       h.Spec,
 							Status: gwapiv1.HTTPRouteStatus{
 								RouteStatus: gwapiv1.RouteStatus{
-									Parents: mergeRouteParentStatus(h.Namespace, r.envoyGateway.Gateway.ControllerName, h.Status.Parents, val.Parents),
+									Parents: mergeRouteParentStatus(h.Namespace, h.Status.Parents, val.Parents),
 								},
 							},
 						}
@@ -147,7 +147,7 @@ func (r *gatewayAPIReconciler) subscribeAndUpdateStatus(ctx context.Context, ext
 							Spec:       g.Spec,
 							Status: gwapiv1.GRPCRouteStatus{
 								RouteStatus: gwapiv1.RouteStatus{
-									Parents: mergeRouteParentStatus(g.Namespace, r.envoyGateway.Gateway.ControllerName, g.Status.Parents, val.Parents),
+									Parents: mergeRouteParentStatus(g.Namespace, g.Status.Parents, val.Parents),
 								},
 							},
 						}
@@ -187,7 +187,7 @@ func (r *gatewayAPIReconciler) subscribeAndUpdateStatus(ctx context.Context, ext
 							Spec:       t.Spec,
 							Status: gwapiv1a2.TLSRouteStatus{
 								RouteStatus: gwapiv1.RouteStatus{
-									Parents: mergeRouteParentStatus(t.Namespace, r.envoyGateway.Gateway.ControllerName, t.Status.Parents, val.Parents),
+									Parents: mergeRouteParentStatus(t.Namespace, t.Status.Parents, val.Parents),
 								},
 							},
 						}
@@ -227,7 +227,7 @@ func (r *gatewayAPIReconciler) subscribeAndUpdateStatus(ctx context.Context, ext
 							Spec:       t.Spec,
 							Status: gwapiv1a2.TCPRouteStatus{
 								RouteStatus: gwapiv1.RouteStatus{
-									Parents: mergeRouteParentStatus(t.Namespace, r.envoyGateway.Gateway.ControllerName, t.Status.Parents, val.Parents),
+									Parents: mergeRouteParentStatus(t.Namespace, t.Status.Parents, val.Parents),
 								},
 							},
 						}
@@ -267,7 +267,7 @@ func (r *gatewayAPIReconciler) subscribeAndUpdateStatus(ctx context.Context, ext
 							Spec:       u.Spec,
 							Status: gwapiv1a2.UDPRouteStatus{
 								RouteStatus: gwapiv1.RouteStatus{
-									Parents: mergeRouteParentStatus(u.Namespace, r.envoyGateway.Gateway.ControllerName, u.Status.Parents, val.Parents),
+									Parents: mergeRouteParentStatus(u.Namespace, u.Status.Parents, val.Parents),
 								},
 							},
 						}
@@ -569,14 +569,15 @@ func (r *gatewayAPIReconciler) subscribeAndUpdateStatus(ctx context.Context, ext
 
 // mergeRouteParentStatus merges the old and new RouteParentStatus.
 // This is needed because the RouteParentStatus doesn't support strategic merge patch yet.
-func mergeRouteParentStatus(ns, controllerName string, old, new []gwapiv1.RouteParentStatus) []gwapiv1.RouteParentStatus {
+func mergeRouteParentStatus(ns string, old, new []gwapiv1.RouteParentStatus) []gwapiv1.RouteParentStatus {
 	// Allocating with worst-case capacity to avoid reallocation.
 	merged := make([]gwapiv1.RouteParentStatus, 0, len(old)+len(new))
 
 	// Range over old status parentRefs in order:
 	// 1. The parentRef exists in the new status: append the new one to the final status.
 	// 2. The parentRef doesn't exist in the new status and it's not our controller: append it to the final status.
-	// 3. The parentRef doesn't exist in the new status, and it is our controller: don't append it to the final status.
+	// 3. The parentRef doesn't exist in the new status, and it is our controller: keep it in the final status.
+	//    This is important for routes with multiple parent references - not all parents are updated in each reconciliation.
 	for _, oldP := range old {
 		found := -1
 		for newI, newP := range new {
@@ -587,7 +588,10 @@ func mergeRouteParentStatus(ns, controllerName string, old, new []gwapiv1.RouteP
 		}
 		if found >= 0 {
 			merged = append(merged, new[found])
-		} else if oldP.ControllerName != gwapiv1.GatewayController(controllerName) {
+		} else {
+			// Keep all old parent statuses, regardless of controller.
+			// For routes with multiple parents managed by the same controller,
+			// not all parents are necessarily updated in each reconciliation.
 			merged = append(merged, oldP)
 		}
 	}
diff --git a/internal/utils/jsonpatch/patch.go b/internal/utils/jsonpatch/patch.go
index 8c14ae19f..868c9c2f3 100644
--- a/internal/utils/jsonpatch/patch.go
+++ b/internal/utils/jsonpatch/patch.go
@@ -11,7 +11,6 @@ import (
 	"fmt"
 
 	jsonpatchv5 "github.com/evanphx/json-patch/v5"
-	"sigs.k8s.io/yaml"
 
 	"github.com/envoyproxy/gateway/internal/ir"
 )
@@ -58,38 +57,18 @@ func ApplyJSONPatches(document json.RawMessage, patches ...ir.JSONPatchOperation
 		}
 
 		for _, path := range jsonPointers {
-			op := ir.JSONPatchOperation{
-				Path:  &path,
-				Op:    p.Op,
-				Value: p.Value,
-				From:  p.From,
-			}
-
-			// Convert patch to JSON
-			// The patch library expects an array so convert it into one
-			y, err := yaml.Marshal([]ir.JSONPatchOperation{op})
-			if err != nil {
-				tErr := fmt.Errorf("unable to marshal patch %+v, err: %s", op, err.Error())
-				tErrs = errors.Join(tErrs, tErr)
-				continue
-			}
-			jsonBytes, err := yaml.YAMLToJSON(y)
+			operation, err := toPatchOperation(path, p)
 			if err != nil {
-				tErr := fmt.Errorf("unable to convert patch to json %s, err: %s", string(y), err.Error())
-				tErrs = errors.Join(tErrs, tErr)
-				continue
-			}
-			patchObj, err := jsonpatchv5.DecodePatch(jsonBytes)
-			if err != nil {
-				tErr := fmt.Errorf("unable to decode patch %s, err: %s", string(jsonBytes), err.Error())
-				tErrs = errors.Join(tErrs, tErr)
+				tErrs = errors.Join(tErrs, err)
 				continue
 			}
 
+			patch := jsonpatchv5.Patch{operation}
+
 			// Apply patch
-			document, err = patchObj.ApplyWithOptions(document, opts)
+			document, err = patch.ApplyWithOptions(document, opts)
 			if err != nil {
-				tErr := fmt.Errorf("unable to apply patch:\n%s on resource:\n%s, err: %s", string(jsonBytes), string(document), err.Error())
+				tErr := fmt.Errorf("unable to apply patch: op=%s path=%s err: %s", string(p.Op), path, err.Error())
 				tErrs = errors.Join(tErrs, tErr)
 				continue
 			}
@@ -97,3 +76,52 @@ func ApplyJSONPatches(document json.RawMessage, patches ...ir.JSONPatchOperation
 	}
 	return document, tErrs
 }
+
+func toPatchOperation(path string, original ir.JSONPatchOperation) (jsonpatchv5.Operation, error) {
+	operation := make(jsonpatchv5.Operation, 4)
+
+	rawOp, err := marshalJSONString(string(original.Op))
+	if err != nil {
+		return nil, fmt.Errorf("unable to marshal patch op %q: %w", string(original.Op), err)
+	}
+	operation["op"] = rawOp
+
+	rawPath, err := marshalJSONString(path)
+	if err != nil {
+		return nil, fmt.Errorf("unable to marshal patch path %q: %w", path, err)
+	}
+	operation["path"] = rawPath
+
+	if original.From != nil {
+		rawFrom, err := marshalJSONString(*original.From)
+		if err != nil {
+			return nil, fmt.Errorf("unable to marshal patch from %q: %w", *original.From, err)
+		}
+		operation["from"] = rawFrom
+	}
+
+	if original.Value != nil {
+		operation["value"] = cloneRawJSON(original.Value.Raw)
+	}
+
+	return operation, nil
+}
+
+func marshalJSONString(value string) (*json.RawMessage, error) {
+	b, err := json.Marshal(value)
+	if err != nil {
+		return nil, err
+	}
+	raw := json.RawMessage(b)
+	return &raw, nil
+}
+
+func cloneRawJSON(src []byte) *json.RawMessage {
+	if src == nil {
+		rawNull := json.RawMessage([]byte("null"))
+		return &rawNull
+	}
+	cloned := make(json.RawMessage, len(src))
+	copy(cloned, src)
+	return &cloned
+}
diff --git a/internal/xds/translator/jsonpatch.go b/internal/xds/translator/jsonpatch.go
index c8b2a7141..e4e3ceb36 100644
--- a/internal/xds/translator/jsonpatch.go
+++ b/internal/xds/translator/jsonpatch.go
@@ -17,7 +17,6 @@ import (
 	tlsv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3"
 	resourcev3 "github.com/envoyproxy/go-control-plane/pkg/resource/v3"
 	"google.golang.org/protobuf/encoding/protojson"
-	"sigs.k8s.io/yaml"
 
 	"github.com/envoyproxy/gateway/internal/gatewayapi/status"
 	"github.com/envoyproxy/gateway/internal/ir"
@@ -68,17 +67,15 @@ func processJSONPatches(tCtx *types.ResourceVersionTable, envoyPatchPolicies []*
 			// If Path and JSONPath is "" and op is "add", unmarshal and add the patch as a complete
 			// resource
 			if p.Operation.Op == ir.JSONPatchOpAdd && p.Operation.IsPathNilOrEmpty() && p.Operation.IsJSONPathNilOrEmpty() {
-				// Convert patch to JSON
-				// The patch library expects an array so convert it into one
-				y, err := yaml.Marshal(p.Operation.Value)
-				if err != nil {
-					tErr := fmt.Errorf("unable to marshal patch %+v, err: %s", p.Operation.Value, err.Error())
+				if p.Operation.Value == nil {
+					tErr := fmt.Errorf("missing value for add operation with empty path")
 					tErrs = errors.Join(tErrs, tErr)
 					continue
 				}
-				jsonBytes, err := yaml.YAMLToJSON(y)
-				if err != nil {
-					tErr := fmt.Errorf("unable to convert patch to json %s, err: %s", string(y), err.Error())
+
+				jsonBytes := p.Operation.Value.Raw
+				if len(jsonBytes) == 0 {
+					tErr := fmt.Errorf("empty value for add operation with empty path")
 					tErrs = errors.Join(tErrs, tErr)
 					continue
 				}
diff --git a/tools/make/kube.mk b/tools/make/kube.mk
index aa0cb495d..843ead2e7 100644
--- a/tools/make/kube.mk
+++ b/tools/make/kube.mk
@@ -25,6 +25,10 @@ BENCHMARK_CONNECTIONS ?= 100
 BENCHMARK_DURATION ?= 60
 BENCHMARK_REPORT_DIR ?= benchmark_report
 
+# Benchmark toggles
+# Disable PNG rendering by default to speed up CI
+BENCHMARK_RENDER_PNG ?= false
+
 CONFORMANCE_RUN_TEST ?=
 
 E2E_RUN_TEST ?=
@@ -253,8 +257,8 @@ run-benchmark: install-benchmark-server prepare-ip-family ## Run benchmark tests
 	kubectl apply -f test/benchmark/config/gatewayclass.yaml
 	go test -v -tags benchmark -timeout $(BENCHMARK_TIMEOUT) ./test/benchmark --rps=$(BENCHMARK_RPS) --connections=$(BENCHMARK_CONNECTIONS) --duration=$(BENCHMARK_DURATION) --report-save-dir=$(BENCHMARK_REPORT_DIR)
 	# render benchmark profiles into image
-	dot -V
-	find test/benchmark/$(BENCHMARK_REPORT_DIR)/profiles -name "*.pprof" -type f -exec sh -c 'go tool pprof -png "$$1" > "$${1%.pprof}.png"' _ {} \;
+	@if [ "$(BENCHMARK_RENDER_PNG)" != "false" ]; then dot -V; fi
+	@if [ "$(BENCHMARK_RENDER_PNG)" != "false" ]; then find test/benchmark/$(BENCHMARK_REPORT_DIR)/profiles -name "*.pprof" -type f -exec sh -c 'go tool pprof -png "$$1" > "$$${1%.pprof}.png"' _ {} \; ; fi
 
 .PHONY: install-benchmark-server
 install-benchmark-server: ## Install nighthawk server for benchmark test



Test patch:
diff --git a/internal/provider/kubernetes/status_test.go b/internal/provider/kubernetes/status_test.go
index 63b7fdea7..1cb4c5730 100644
--- a/internal/provider/kubernetes/status_test.go
+++ b/internal/provider/kubernetes/status_test.go
@@ -361,6 +361,24 @@ func Test_mergeRouteParentStatus(t *testing.T) {
 						},
 					},
 				},
+				{
+					ControllerName: "gateway.envoyproxy.io/gatewayclass-controller",
+					ParentRef: gwapiv1.ParentReference{
+						Name: "gateway2",
+					},
+					Conditions: []metav1.Condition{
+						{
+							Type:   string(gwapiv1.RouteConditionAccepted),
+							Status: metav1.ConditionTrue,
+							Reason: "Accepted",
+						},
+						{
+							Type:   string(gwapiv1.RouteConditionResolvedRefs),
+							Status: metav1.ConditionTrue,
+							Reason: "ResolvedRefs",
+						},
+					},
+				},
 				{
 					ControllerName: "gateway.envoyproxy.io/gatewayclass-controller",
 					ParentRef: gwapiv1.ParentReference{
@@ -456,6 +474,24 @@ func Test_mergeRouteParentStatus(t *testing.T) {
 						},
 					},
 				},
+				{
+					ControllerName: "gateway.envoyproxy.io/gatewayclass-controller",
+					ParentRef: gwapiv1.ParentReference{
+						Name: "gateway2",
+					},
+					Conditions: []metav1.Condition{
+						{
+							Type:   string(gwapiv1.RouteConditionAccepted),
+							Status: metav1.ConditionTrue,
+							Reason: "Accepted",
+						},
+						{
+							Type:   string(gwapiv1.RouteConditionResolvedRefs),
+							Status: metav1.ConditionTrue,
+							Reason: "ResolvedRefs",
+						},
+					},
+				},
 			},
 		},
 
@@ -670,6 +706,24 @@ func Test_mergeRouteParentStatus(t *testing.T) {
 				},
 			},
 			want: []gwapiv1.RouteParentStatus{
+				{
+					ControllerName: "gateway.envoyproxy.io/gatewayclass-controller",
+					ParentRef: gwapiv1.ParentReference{
+						Name: "gateway2",
+					},
+					Conditions: []metav1.Condition{
+						{
+							Type:   string(gwapiv1.RouteConditionAccepted),
+							Status: metav1.ConditionTrue,
+							Reason: "Accepted",
+						},
+						{
+							Type:   string(gwapiv1.RouteConditionResolvedRefs),
+							Status: metav1.ConditionTrue,
+							Reason: "ResolvedRefs",
+						},
+					},
+				},
 				{
 					ControllerName: "gateway.envoyproxy.io/gatewayclass-controller",
 					ParentRef: gwapiv1.ParentReference{
@@ -690,9 +744,10 @@ func Test_mergeRouteParentStatus(t *testing.T) {
 				},
 			},
 		},
-		// Similar note about practicality of occurrence.
+		// Test that parent refs managed by our controller are preserved even when not in new update.
+		// This is important for routes with multiple parent references.
 		{
-			name: "old contains one parentRef of ours, and it gets dropped in new.",
+			name: "old contains one parentRef of ours, and it's not in new - should be preserved.",
 			args: args{
 				old: []gwapiv1.RouteParentStatus{
 					{
@@ -716,12 +771,118 @@ func Test_mergeRouteParentStatus(t *testing.T) {
 				},
 				new: []gwapiv1.RouteParentStatus{},
 			},
-			want: []gwapiv1.RouteParentStatus{},
+			want: []gwapiv1.RouteParentStatus{
+				{
+					ControllerName: "gateway.envoyproxy.io/gatewayclass-controller",
+					ParentRef: gwapiv1.ParentReference{
+						Name: "gateway2",
+					},
+					Conditions: []metav1.Condition{
+						{
+							Type:   string(gwapiv1.RouteConditionAccepted),
+							Status: metav1.ConditionTrue,
+							Reason: "Accepted",
+						},
+						{
+							Type:   string(gwapiv1.RouteConditionResolvedRefs),
+							Status: metav1.ConditionTrue,
+							Reason: "ResolvedRefs",
+						},
+					},
+				},
+			},
+		},
+		// Test multi-parent scenario where only one parent is updated at a time.
+		{
+			name: "multiple parents from same controller - update one, preserve others",
+			args: args{
+				old: []gwapiv1.RouteParentStatus{
+					{
+						ControllerName: "gateway.envoyproxy.io/gatewayclass-controller",
+						ParentRef: gwapiv1.ParentReference{
+							Name: "gateway1",
+						},
+						Conditions: []metav1.Condition{
+							{
+								Type:   string(gwapiv1.RouteConditionAccepted),
+								Status: metav1.ConditionTrue,
+								Reason: "Accepted",
+							},
+						},
+					},
+					{
+						ControllerName: "gateway.envoyproxy.io/gatewayclass-controller",
+						ParentRef: gwapiv1.ParentReference{
+							Name: "gateway2",
+						},
+						Conditions: []metav1.Condition{
+							{
+								Type:   string(gwapiv1.RouteConditionAccepted),
+								Status: metav1.ConditionTrue,
+								Reason: "Accepted",
+							},
+						},
+					},
+				},
+				new: []gwapiv1.RouteParentStatus{
+					{
+						ControllerName: "gateway.envoyproxy.io/gatewayclass-controller",
+						ParentRef: gwapiv1.ParentReference{
+							Name: "gateway1",
+						},
+						Conditions: []metav1.Condition{
+							{
+								Type:   string(gwapiv1.RouteConditionAccepted),
+								Status: metav1.ConditionTrue,
+								Reason: "Accepted",
+							},
+							{
+								Type:   string(gwapiv1.RouteConditionResolvedRefs),
+								Status: metav1.ConditionTrue,
+								Reason: "ResolvedRefs",
+							},
+						},
+					},
+				},
+			},
+			want: []gwapiv1.RouteParentStatus{
+				{
+					ControllerName: "gateway.envoyproxy.io/gatewayclass-controller",
+					ParentRef: gwapiv1.ParentReference{
+						Name: "gateway1",
+					},
+					Conditions: []metav1.Condition{
+						{
+							Type:   string(gwapiv1.RouteConditionAccepted),
+							Status: metav1.ConditionTrue,
+							Reason: "Accepted",
+						},
+						{
+							Type:   string(gwapiv1.RouteConditionResolvedRefs),
+							Status: metav1.ConditionTrue,
+							Reason: "ResolvedRefs",
+						},
+					},
+				},
+				{
+					ControllerName: "gateway.envoyproxy.io/gatewayclass-controller",
+					ParentRef: gwapiv1.ParentReference{
+						Name: "gateway2",
+					},
+					Conditions: []metav1.Condition{
+						{
+							Type:   string(gwapiv1.RouteConditionAccepted),
+							Status: metav1.ConditionTrue,
+							Reason: "Accepted",
+						},
+					},
+				},
+			},
 		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			if got := mergeRouteParentStatus("default", "gateway.envoyproxy.io/gatewayclass-controller", tt.args.old, tt.args.new); !reflect.DeepEqual(got, tt.want) {
+			if got := mergeRouteParentStatus("default", tt.args.old, tt.args.new); !reflect.DeepEqual(got, tt.want) {
 				t.Errorf("mergeRouteParentStatus() = %v, want %v", got, tt.want)
 			}
 		})
diff --git a/test/benchmark/config/nighthawk-client.yaml b/test/benchmark/config/nighthawk-client.yaml
index 35553eda6..6d689a203 100644
--- a/test/benchmark/config/nighthawk-client.yaml
+++ b/test/benchmark/config/nighthawk-client.yaml
@@ -9,7 +9,7 @@ spec:
     spec:
       containers:
         - name: nighthawk-client
-          image: envoyproxy/nighthawk-dev:latest
+          image: envoyproxy/nighthawk-dev:6198df0e01dc85f8d5efa3ef188f3bcdcc6eecac
           imagePullPolicy: IfNotPresent
           command:
             - /usr/local/bin/nighthawk_client
diff --git a/test/benchmark/config/nighthawk-test-server.yaml b/test/benchmark/config/nighthawk-test-server.yaml
index 75fa0bfbd..c1ce968e6 100644
--- a/test/benchmark/config/nighthawk-test-server.yaml
+++ b/test/benchmark/config/nighthawk-test-server.yaml
@@ -17,7 +17,7 @@ spec:
       serviceAccountName: default
       containers:
         - name: nighthawk-server
-          image: envoyproxy/nighthawk-dev:latest
+          image: envoyproxy/nighthawk-dev:6198df0e01dc85f8d5efa3ef188f3bcdcc6eecac
           imagePullPolicy: IfNotPresent
           args: ["-c", "/etc/test-server-config/nighthawk-test-server-config.yaml"]
           ports: