Repository: envoyproxy/gateway

Issue:
Support for targeting sections (Gateway listener/Route Rule) in BackendTrafficPolicy
*Description*:
>Describe the desired behavior, what scenario it enables and how it
would be used.

This is an Issue that helps you keep track of multiple, scattered issues.
Relates to https://github.com/envoyproxy/gateway/issues/2054 & https://github.com/envoyproxy/gateway/issues/4085

[optional *Relevant Links*:]
>Any extra documentation required to understand the issue.


Hints from issue comments:
@github-actions: This issue has been automatically marked as stale because it has not had activity in the last 30 days.

Solution patch:
diff --git a/api/v1alpha1/backendtrafficpolicy_types.go b/api/v1alpha1/backendtrafficpolicy_types.go
index 6b3ffff1f..967985426 100644
--- a/api/v1alpha1/backendtrafficpolicy_types.go
+++ b/api/v1alpha1/backendtrafficpolicy_types.go
@@ -39,10 +39,8 @@ type BackendTrafficPolicy struct {
 // +kubebuilder:validation:XValidation:rule="(has(self.targetRef) && !has(self.targetRefs)) || (!has(self.targetRef) && has(self.targetRefs)) || (has(self.targetSelectors) && self.targetSelectors.size() > 0) ", message="either targetRef or targetRefs must be used"
 // +kubebuilder:validation:XValidation:rule="has(self.targetRef) ? self.targetRef.group == 'gateway.networking.k8s.io' : true ", message="this policy can only have a targetRef.group of gateway.networking.k8s.io"
 // +kubebuilder:validation:XValidation:rule="has(self.targetRef) ? self.targetRef.kind in ['Gateway', 'HTTPRoute', 'GRPCRoute', 'UDPRoute', 'TCPRoute', 'TLSRoute'] : true", message="this policy can only have a targetRef.kind of Gateway/HTTPRoute/GRPCRoute/TCPRoute/UDPRoute/TLSRoute"
-// +kubebuilder:validation:XValidation:rule="has(self.targetRef) ? !has(self.targetRef.sectionName) : true",message="this policy does not yet support the sectionName field"
 // +kubebuilder:validation:XValidation:rule="has(self.targetRefs) ? self.targetRefs.all(ref, ref.group == 'gateway.networking.k8s.io') : true ", message="this policy can only have a targetRefs[*].group of gateway.networking.k8s.io"
 // +kubebuilder:validation:XValidation:rule="has(self.targetRefs) ? self.targetRefs.all(ref, ref.kind in ['Gateway', 'HTTPRoute', 'GRPCRoute', 'UDPRoute', 'TCPRoute', 'TLSRoute']) : true ", message="this policy can only have a targetRefs[*].kind of Gateway/HTTPRoute/GRPCRoute/TCPRoute/UDPRoute/TLSRoute"
-// +kubebuilder:validation:XValidation:rule="has(self.targetRefs) ? self.targetRefs.all(ref, !has(ref.sectionName)) : true",message="this policy does not yet support the sectionName field"
 // +kubebuilder:validation:XValidation:rule="!has(self.compression) || !has(self.compressor)", message="either compression or compressor can be set, not both"
 type BackendTrafficPolicySpec struct {
 	PolicyTargetReferences `json:",inline"`
diff --git a/api/v1alpha1/clienttrafficpolicy_types.go b/api/v1alpha1/clienttrafficpolicy_types.go
index ed0ab8724..a416e5487 100644
--- a/api/v1alpha1/clienttrafficpolicy_types.go
+++ b/api/v1alpha1/clienttrafficpolicy_types.go
@@ -141,13 +141,14 @@ type HeaderSettings struct {
 	// PreserveXRequestID configures Envoy to keep the X-Request-ID header if passed for a request that is edge
 	// (Edge request is the request from external clients to front Envoy) and not reset it, which is the current Envoy behaviour.
 	// Defaults to false and cannot be combined with RequestID.
-	// Deprecated: use RequestID=Preserve instead
+	// Deprecated: use RequestID=PreserveOrGenerate instead
 	//
 	// +optional
 	PreserveXRequestID *bool `json:"preserveXRequestID,omitempty"`
 
 	// RequestID configures Envoy's behavior for handling the `X-Request-ID` header.
-	// Defaults to `Generate` and builds the `X-Request-ID` for every request and ignores pre-existing values from the edge.
+	// When omitted default behavior is `Generate` which builds the `X-Request-ID` for every request
+	//  and ignores pre-existing values from the edge.
 	// (An "edge request" refers to a request from an external client to the Envoy entrypoint.)
 	//
 	// +optional
@@ -182,7 +183,8 @@ const (
 	WithUnderscoresActionDropHeader WithUnderscoresAction = "DropHeader"
 )
 
-// RequestIDAction configures Envoy's behavior for handling the `X-Request-ID` header.
+// RequestIDAction configures Envoy's behavior for handling the `X-Request-ID` header at the edge.
+// An "edge request" refers to a request from an external client to the Envoy entrypoint.
 //
 // +kubebuilder:validation:Enum=PreserveOrGenerate;Preserve;Generate;Disable
 type RequestIDAction string
diff --git a/api/v1alpha1/envoygateway_types.go b/api/v1alpha1/envoygateway_types.go
index 7f2d6723c..42dd3adfc 100644
--- a/api/v1alpha1/envoygateway_types.go
+++ b/api/v1alpha1/envoygateway_types.go
@@ -269,6 +269,11 @@ type EnvoyGatewayKubernetesProvider struct {
 	// +optional
 	RateLimitHpa *KubernetesHorizontalPodAutoscalerSpec `json:"rateLimitHpa,omitempty"`
 
+	// RateLimitPDB allows to control the pod disruption budget of rate limit service.
+	//
+	// +optional
+	RateLimitPDB *KubernetesPodDisruptionBudgetSpec `json:"rateLimitPDB,omitempty"`
+
 	// Watch holds configuration of which input resources should be watched and reconciled.
 	// +optional
 	Watch *KubernetesWatchMode `json:"watch,omitempty"`
diff --git a/api/v1alpha1/ratelimit_types.go b/api/v1alpha1/ratelimit_types.go
index dee9155c1..bc895a244 100644
--- a/api/v1alpha1/ratelimit_types.go
+++ b/api/v1alpha1/ratelimit_types.go
@@ -5,6 +5,10 @@
 
 package v1alpha1
 
+import (
+	gwapiv1 "sigs.k8s.io/gateway-api/apis/v1"
+)
+
 // RateLimitSpec defines the desired state of RateLimitSpec.
 // +union
 type RateLimitSpec struct {
@@ -183,17 +187,30 @@ type RateLimitCostMetadata struct {
 // RateLimitSelectCondition specifies the attributes within the traffic flow that can
 // be used to select a subset of clients to be ratelimited.
 // All the individual conditions must hold True for the overall condition to hold True.
+// And, at least one of headers or methods or path or sourceCIDR condition must be specified.
+//
+// +kubebuilder:validation:XValidation:rule="has(self.headers) || has(self.methods) || has(self.path) || has(self.sourceCIDR)",message="at least one of headers, methods, path or sourceCIDR must be specified"
 type RateLimitSelectCondition struct {
 	// Headers is a list of request headers to match. Multiple header values are ANDed together,
 	// meaning, a request MUST match all the specified headers.
-	// At least one of headers or sourceCIDR condition must be specified.
 	//
 	// +optional
 	// +kubebuilder:validation:MaxItems=16
 	Headers []HeaderMatch `json:"headers,omitempty"`
 
+	// Methods is a list of request methods to match. Multiple method values are ORed together,
+	// meaning, a request can match any one of the specified methods. If not specified, it matches all methods.
+	//
+	// +optional
+	Methods []MethodMatch `json:"methods,omitempty"`
+
+	// Path is the request path to match.
+	// Support Exact, PathPrefix and RegularExpression match types.
+	//
+	// +optional
+	Path *PathMatch `json:"path,omitempty"`
+
 	// SourceCIDR is the client IP Address range to match on.
-	// At least one of headers or sourceCIDR condition must be specified.
 	//
 	// +optional
 	SourceCIDR *SourceMatch `json:"sourceCIDR,omitempty"`
@@ -278,6 +295,39 @@ const (
 	HeaderMatchDistinct HeaderMatchType = "Distinct"
 )
 
+// MethodMatch defines the matching criteria for the HTTP method of a request.
+type MethodMatch struct {
+	// Value specifies the HTTP method.
+	Value gwapiv1.HTTPMethod `json:"value"`
+
+	// Invert specifies whether the value match result will be inverted.
+	//
+	// +optional
+	// +kubebuilder:default=false
+	Invert *bool `json:"invert,omitempty"`
+}
+
+// PathMatch defines the matching criteria for the HTTP path of a request.
+type PathMatch struct {
+	// Type specifies how to match against the value of the path.
+	//
+	// +optional
+	// +kubebuilder:default=PathPrefix
+	Type *gwapiv1.PathMatchType `json:"type,omitempty"`
+
+	// Value specifies the HTTP path.
+	//
+	// +kubebuilder:default="/"
+	// +kubebuilder:validation:MaxLength=1024
+	Value string `json:"value"`
+
+	// Invert specifies whether the value match result will be inverted.
+	//
+	// +optional
+	// +kubebuilder:default=false
+	Invert *bool `json:"invert,omitempty"`
+}
+
 // RateLimitValue defines the limits for rate limiting.
 type RateLimitValue struct {
 	Requests uint          `json:"requests"`
diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go
index abef1052a..c06d4d02c 100644
--- a/api/v1alpha1/zz_generated.deepcopy.go
+++ b/api/v1alpha1/zz_generated.deepcopy.go
@@ -2022,6 +2022,11 @@ func (in *EnvoyGatewayKubernetesProvider) DeepCopyInto(out *EnvoyGatewayKubernet
 		*out = new(KubernetesHorizontalPodAutoscalerSpec)
 		(*in).DeepCopyInto(*out)
 	}
+	if in.RateLimitPDB != nil {
+		in, out := &in.RateLimitPDB, &out.RateLimitPDB
+		*out = new(KubernetesPodDisruptionBudgetSpec)
+		(*in).DeepCopyInto(*out)
+	}
 	if in.Watch != nil {
 		in, out := &in.Watch, &out.Watch
 		*out = new(KubernetesWatchMode)
@@ -4858,6 +4863,26 @@ func (in *Lua) DeepCopy() *Lua {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *MethodMatch) DeepCopyInto(out *MethodMatch) {
+	*out = *in
+	if in.Invert != nil {
+		in, out := &in.Invert, &out.Invert
+		*out = new(bool)
+		**out = **in
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MethodMatch.
+func (in *MethodMatch) DeepCopy() *MethodMatch {
+	if in == nil {
+		return nil
+	}
+	out := new(MethodMatch)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *OIDC) DeepCopyInto(out *OIDC) {
 	*out = *in
@@ -5193,6 +5218,31 @@ func (in *PassiveHealthCheck) DeepCopy() *PassiveHealthCheck {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *PathMatch) DeepCopyInto(out *PathMatch) {
+	*out = *in
+	if in.Type != nil {
+		in, out := &in.Type, &out.Type
+		*out = new(v1.PathMatchType)
+		**out = **in
+	}
+	if in.Invert != nil {
+		in, out := &in.Invert, &out.Invert
+		*out = new(bool)
+		**out = **in
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PathMatch.
+func (in *PathMatch) DeepCopy() *PathMatch {
+	if in == nil {
+		return nil
+	}
+	out := new(PathMatch)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *PathSettings) DeepCopyInto(out *PathSettings) {
 	*out = *in
@@ -6026,6 +6076,18 @@ func (in *RateLimitSelectCondition) DeepCopyInto(out *RateLimitSelectCondition)
 			(*in)[i].DeepCopyInto(&(*out)[i])
 		}
 	}
+	if in.Methods != nil {
+		in, out := &in.Methods, &out.Methods
+		*out = make([]MethodMatch, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+	if in.Path != nil {
+		in, out := &in.Path, &out.Path
+		*out = new(PathMatch)
+		(*in).DeepCopyInto(*out)
+	}
 	if in.SourceCIDR != nil {
 		in, out := &in.SourceCIDR, &out.SourceCIDR
 		*out = new(SourceMatch)
diff --git a/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml b/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml
index ecc9059d8..180277e12 100644
--- a/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml
+++ b/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml
@@ -948,12 +948,12 @@ spec:
                                   RateLimitSelectCondition specifies the attributes within the traffic flow that can
                                   be used to select a subset of clients to be ratelimited.
                                   All the individual conditions must hold True for the overall condition to hold True.
+                                  And, at least one of headers or methods or path or sourceCIDR condition must be specified.
                                 properties:
                                   headers:
                                     description: |-
                                       Headers is a list of request headers to match. Multiple header values are ANDed together,
                                       meaning, a request MUST match all the specified headers.
-                                      At least one of headers or sourceCIDR condition must be specified.
                                     items:
                                       description: HeaderMatch defines the match attributes
                                         within the HTTP Headers of the request.
@@ -994,10 +994,66 @@ spec:
                                       type: object
                                     maxItems: 16
                                     type: array
-                                  sourceCIDR:
+                                  methods:
+                                    description: |-
+                                      Methods is a list of request methods to match. Multiple method values are ORed together,
+                                      meaning, a request can match any one of the specified methods. If not specified, it matches all methods.
+                                    items:
+                                      description: MethodMatch defines the matching
+                                        criteria for the HTTP method of a request.
+                                      properties:
+                                        invert:
+                                          default: false
+                                          description: Invert specifies whether the
+                                            value match result will be inverted.
+                                          type: boolean
+                                        value:
+                                          description: Value specifies the HTTP method.
+                                          enum:
+                                          - GET
+                                          - HEAD
+                                          - POST
+                                          - PUT
+                                          - DELETE
+                                          - CONNECT
+                                          - OPTIONS
+                                          - TRACE
+                                          - PATCH
+                                          type: string
+                                      required:
+                                      - value
+                                      type: object
+                                    type: array
+                                  path:
                                     description: |-
-                                      SourceCIDR is the client IP Address range to match on.
-                                      At least one of headers or sourceCIDR condition must be specified.
+                                      Path is the request path to match.
+                                      Support Exact, PathPrefix and RegularExpression match types.
+                                    properties:
+                                      invert:
+                                        default: false
+                                        description: Invert specifies whether the
+                                          value match result will be inverted.
+                                        type: boolean
+                                      type:
+                                        default: PathPrefix
+                                        description: Type specifies how to match against
+                                          the value of the path.
+                                        enum:
+                                        - Exact
+                                        - PathPrefix
+                                        - RegularExpression
+                                        type: string
+                                      value:
+                                        default: /
+                                        description: Value specifies the HTTP path.
+                                        maxLength: 1024
+                                        type: string
+                                    required:
+                                    - value
+                                    type: object
+                                  sourceCIDR:
+                                    description: SourceCIDR is the client IP Address
+                                      range to match on.
                                     properties:
                                       type:
                                         default: Exact
@@ -1017,6 +1073,11 @@ spec:
                                     - value
                                     type: object
                                 type: object
+                                x-kubernetes-validations:
+                                - message: at least one of headers, methods, path
+                                    or sourceCIDR must be specified
+                                  rule: has(self.headers) || has(self.methods) ||
+                                    has(self.path) || has(self.sourceCIDR)
                               maxItems: 8
                               type: array
                             cost:
@@ -1199,12 +1260,12 @@ spec:
                                   RateLimitSelectCondition specifies the attributes within the traffic flow that can
                                   be used to select a subset of clients to be ratelimited.
                                   All the individual conditions must hold True for the overall condition to hold True.
+                                  And, at least one of headers or methods or path or sourceCIDR condition must be specified.
                                 properties:
                                   headers:
                                     description: |-
                                       Headers is a list of request headers to match. Multiple header values are ANDed together,
                                       meaning, a request MUST match all the specified headers.
-                                      At least one of headers or sourceCIDR condition must be specified.
                                     items:
                                       description: HeaderMatch defines the match attributes
                                         within the HTTP Headers of the request.
@@ -1245,10 +1306,66 @@ spec:
                                       type: object
                                     maxItems: 16
                                     type: array
-                                  sourceCIDR:
+                                  methods:
+                                    description: |-
+                                      Methods is a list of request methods to match. Multiple method values are ORed together,
+                                      meaning, a request can match any one of the specified methods. If not specified, it matches all methods.
+                                    items:
+                                      description: MethodMatch defines the matching
+                                        criteria for the HTTP method of a request.
+                                      properties:
+                                        invert:
+                                          default: false
+                                          description: Invert specifies whether the
+                                            value match result will be inverted.
+                                          type: boolean
+                                        value:
+                                          description: Value specifies the HTTP method.
+                                          enum:
+                                          - GET
+                                          - HEAD
+                                          - POST
+                                          - PUT
+                                          - DELETE
+                                          - CONNECT
+                                          - OPTIONS
+                                          - TRACE
+                                          - PATCH
+                                          type: string
+                                      required:
+                                      - value
+                                      type: object
+                                    type: array
+                                  path:
                                     description: |-
-                                      SourceCIDR is the client IP Address range to match on.
-                                      At least one of headers or sourceCIDR condition must be specified.
+                                      Path is the request path to match.
+                                      Support Exact, PathPrefix and RegularExpression match types.
+                                    properties:
+                                      invert:
+                                        default: false
+                                        description: Invert specifies whether the
+                                          value match result will be inverted.
+                                        type: boolean
+                                      type:
+                                        default: PathPrefix
+                                        description: Type specifies how to match against
+                                          the value of the path.
+                                        enum:
+                                        - Exact
+                                        - PathPrefix
+                                        - RegularExpression
+                                        type: string
+                                      value:
+                                        default: /
+                                        description: Value specifies the HTTP path.
+                                        maxLength: 1024
+                                        type: string
+                                    required:
+                                    - value
+                                    type: object
+                                  sourceCIDR:
+                                    description: SourceCIDR is the client IP Address
+                                      range to match on.
                                     properties:
                                       type:
                                         default: Exact
@@ -1268,6 +1385,11 @@ spec:
                                     - value
                                     type: object
                                 type: object
+                                x-kubernetes-validations:
+                                - message: at least one of headers, methods, path
+                                    or sourceCIDR must be specified
+                                  rule: has(self.headers) || has(self.methods) ||
+                                    has(self.path) || has(self.sourceCIDR)
                               maxItems: 8
                               type: array
                             cost:
@@ -2294,8 +2416,6 @@ spec:
             - message: this policy can only have a targetRef.kind of Gateway/HTTPRoute/GRPCRoute/TCPRoute/UDPRoute/TLSRoute
               rule: 'has(self.targetRef) ? self.targetRef.kind in [''Gateway'', ''HTTPRoute'',
                 ''GRPCRoute'', ''UDPRoute'', ''TCPRoute'', ''TLSRoute''] : true'
-            - message: this policy does not yet support the sectionName field
-              rule: 'has(self.targetRef) ? !has(self.targetRef.sectionName) : true'
             - message: this policy can only have a targetRefs[*].group of gateway.networking.k8s.io
               rule: 'has(self.targetRefs) ? self.targetRefs.all(ref, ref.group ==
                 ''gateway.networking.k8s.io'') : true '
@@ -2303,9 +2423,6 @@ spec:
               rule: 'has(self.targetRefs) ? self.targetRefs.all(ref, ref.kind in [''Gateway'',
                 ''HTTPRoute'', ''GRPCRoute'', ''UDPRoute'', ''TCPRoute'', ''TLSRoute''])
                 : true '
-            - message: this policy does not yet support the sectionName field
-              rule: 'has(self.targetRefs) ? self.targetRefs.all(ref, !has(ref.sectionName))
-                : true'
             - message: either compression or compressor can be set, not both
               rule: '!has(self.compression) || !has(self.compressor)'
             - message: predictivePercent in preconnect policy only works with RoundRobin
diff --git a/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_clienttrafficpolicies.yaml b/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_clienttrafficpolicies.yaml
index 1efacd8d2..e5aade44e 100644
--- a/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_clienttrafficpolicies.yaml
+++ b/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_clienttrafficpolicies.yaml
@@ -492,12 +492,13 @@ spec:
                       PreserveXRequestID configures Envoy to keep the X-Request-ID header if passed for a request that is edge
                       (Edge request is the request from external clients to front Envoy) and not reset it, which is the current Envoy behaviour.
                       Defaults to false and cannot be combined with RequestID.
-                      Deprecated: use RequestID=Preserve instead
+                      Deprecated: use RequestID=PreserveOrGenerate instead
                     type: boolean
                   requestID:
                     description: |-
                       RequestID configures Envoy's behavior for handling the `X-Request-ID` header.
-                      Defaults to `Generate` and builds the `X-Request-ID` for every request and ignores pre-existing values from the edge.
+                      When omitted default behavior is `Generate` which builds the `X-Request-ID` for every request
+                       and ignores pre-existing values from the edge.
                       (An "edge request" refers to a request from an external client to the Envoy entrypoint.)
                     enum:
                     - PreserveOrGenerate
diff --git a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml
index 125c4ddf8..42308f8f3 100644
--- a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml
+++ b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml
@@ -947,12 +947,12 @@ spec:
                                   RateLimitSelectCondition specifies the attributes within the traffic flow that can
                                   be used to select a subset of clients to be ratelimited.
                                   All the individual conditions must hold True for the overall condition to hold True.
+                                  And, at least one of headers or methods or path or sourceCIDR condition must be specified.
                                 properties:
                                   headers:
                                     description: |-
                                       Headers is a list of request headers to match. Multiple header values are ANDed together,
                                       meaning, a request MUST match all the specified headers.
-                                      At least one of headers or sourceCIDR condition must be specified.
                                     items:
                                       description: HeaderMatch defines the match attributes
                                         within the HTTP Headers of the request.
@@ -993,10 +993,66 @@ spec:
                                       type: object
                                     maxItems: 16
                                     type: array
-                                  sourceCIDR:
+                                  methods:
+                                    description: |-
+                                      Methods is a list of request methods to match. Multiple method values are ORed together,
+                                      meaning, a request can match any one of the specified methods. If not specified, it matches all methods.
+                                    items:
+                                      description: MethodMatch defines the matching
+                                        criteria for the HTTP method of a request.
+                                      properties:
+                                        invert:
+                                          default: false
+                                          description: Invert specifies whether the
+                                            value match result will be inverted.
+                                          type: boolean
+                                        value:
+                                          description: Value specifies the HTTP method.
+                                          enum:
+                                          - GET
+                                          - HEAD
+                                          - POST
+                                          - PUT
+                                          - DELETE
+                                          - CONNECT
+                                          - OPTIONS
+                                          - TRACE
+                                          - PATCH
+                                          type: string
+                                      required:
+                                      - value
+                                      type: object
+                                    type: array
+                                  path:
                                     description: |-
-                                      SourceCIDR is the client IP Address range to match on.
-                                      At least one of headers or sourceCIDR condition must be specified.
+                                      Path is the request path to match.
+                                      Support Exact, PathPrefix and RegularExpression match types.
+                                    properties:
+                                      invert:
+                                        default: false
+                                        description: Invert specifies whether the
+                                          value match result will be inverted.
+                                        type: boolean
+                                      type:
+                                        default: PathPrefix
+                                        description: Type specifies how to match against
+                                          the value of the path.
+                                        enum:
+                                        - Exact
+                                        - PathPrefix
+                                        - RegularExpression
+                                        type: string
+                                      value:
+                                        default: /
+                                        description: Value specifies the HTTP path.
+                                        maxLength: 1024
+                                        type: string
+                                    required:
+                                    - value
+                                    type: object
+                                  sourceCIDR:
+                                    description: SourceCIDR is the client IP Address
+                                      range to match on.
                                     properties:
                                       type:
                                         default: Exact
@@ -1016,6 +1072,11 @@ spec:
                                     - value
                                     type: object
                                 type: object
+                                x-kubernetes-validations:
+                                - message: at least one of headers, methods, path
+                                    or sourceCIDR must be specified
+                                  rule: has(self.headers) || has(self.methods) ||
+                                    has(self.path) || has(self.sourceCIDR)
                               maxItems: 8
                               type: array
                             cost:
@@ -1198,12 +1259,12 @@ spec:
                                   RateLimitSelectCondition specifies the attributes within the traffic flow that can
                                   be used to select a subset of clients to be ratelimited.
                                   All the individual conditions must hold True for the overall condition to hold True.
+                                  And, at least one of headers or methods or path or sourceCIDR condition must be specified.
                                 properties:
                                   headers:
                                     description: |-
                                       Headers is a list of request headers to match. Multiple header values are ANDed together,
                                       meaning, a request MUST match all the specified headers.
-                                      At least one of headers or sourceCIDR condition must be specified.
                                     items:
                                       description: HeaderMatch defines the match attributes
                                         within the HTTP Headers of the request.
@@ -1244,10 +1305,66 @@ spec:
                                       type: object
                                     maxItems: 16
                                     type: array
-                                  sourceCIDR:
+                                  methods:
+                                    description: |-
+                                      Methods is a list of request methods to match. Multiple method values are ORed together,
+                                      meaning, a request can match any one of the specified methods. If not specified, it matches all methods.
+                                    items:
+                                      description: MethodMatch defines the matching
+                                        criteria for the HTTP method of a request.
+                                      properties:
+                                        invert:
+                                          default: false
+                                          description: Invert specifies whether the
+                                            value match result will be inverted.
+                                          type: boolean
+                                        value:
+                                          description: Value specifies the HTTP method.
+                                          enum:
+                                          - GET
+                                          - HEAD
+                                          - POST
+                                          - PUT
+                                          - DELETE
+                                          - CONNECT
+                                          - OPTIONS
+                                          - TRACE
+                                          - PATCH
+                                          type: string
+                                      required:
+                                      - value
+                                      type: object
+                                    type: array
+                                  path:
                                     description: |-
-                                      SourceCIDR is the client IP Address range to match on.
-                                      At least one of headers or sourceCIDR condition must be specified.
+                                      Path is the request path to match.
+                                      Support Exact, PathPrefix and RegularExpression match types.
+                                    properties:
+                                      invert:
+                                        default: false
+                                        description: Invert specifies whether the
+                                          value match result will be inverted.
+                                        type: boolean
+                                      type:
+                                        default: PathPrefix
+                                        description: Type specifies how to match against
+                                          the value of the path.
+                                        enum:
+                                        - Exact
+                                        - PathPrefix
+                                        - RegularExpression
+                                        type: string
+                                      value:
+                                        default: /
+                                        description: Value specifies the HTTP path.
+                                        maxLength: 1024
+                                        type: string
+                                    required:
+                                    - value
+                                    type: object
+                                  sourceCIDR:
+                                    description: SourceCIDR is the client IP Address
+                                      range to match on.
                                     properties:
                                       type:
                                         default: Exact
@@ -1267,6 +1384,11 @@ spec:
                                     - value
                                     type: object
                                 type: object
+                                x-kubernetes-validations:
+                                - message: at least one of headers, methods, path
+                                    or sourceCIDR must be specified
+                                  rule: has(self.headers) || has(self.methods) ||
+                                    has(self.path) || has(self.sourceCIDR)
                               maxItems: 8
                               type: array
                             cost:
@@ -2293,8 +2415,6 @@ spec:
             - message: this policy can only have a targetRef.kind of Gateway/HTTPRoute/GRPCRoute/TCPRoute/UDPRoute/TLSRoute
               rule: 'has(self.targetRef) ? self.targetRef.kind in [''Gateway'', ''HTTPRoute'',
                 ''GRPCRoute'', ''UDPRoute'', ''TCPRoute'', ''TLSRoute''] : true'
-            - message: this policy does not yet support the sectionName field
-              rule: 'has(self.targetRef) ? !has(self.targetRef.sectionName) : true'
             - message: this policy can only have a targetRefs[*].group of gateway.networking.k8s.io
               rule: 'has(self.targetRefs) ? self.targetRefs.all(ref, ref.group ==
                 ''gateway.networking.k8s.io'') : true '
@@ -2302,9 +2422,6 @@ spec:
               rule: 'has(self.targetRefs) ? self.targetRefs.all(ref, ref.kind in [''Gateway'',
                 ''HTTPRoute'', ''GRPCRoute'', ''UDPRoute'', ''TCPRoute'', ''TLSRoute''])
                 : true '
-            - message: this policy does not yet support the sectionName field
-              rule: 'has(self.targetRefs) ? self.targetRefs.all(ref, !has(ref.sectionName))
-                : true'
             - message: either compression or compressor can be set, not both
               rule: '!has(self.compression) || !has(self.compressor)'
             - message: predictivePercent in preconnect policy only works with RoundRobin
diff --git a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_clienttrafficpolicies.yaml b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_clienttrafficpolicies.yaml
index 6bb3dc633..56ec55970 100644
--- a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_clienttrafficpolicies.yaml
+++ b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_clienttrafficpolicies.yaml
@@ -491,12 +491,13 @@ spec:
                       PreserveXRequestID configures Envoy to keep the X-Request-ID header if passed for a request that is edge
                       (Edge request is the request from external clients to front Envoy) and not reset it, which is the current Envoy behaviour.
                       Defaults to false and cannot be combined with RequestID.
-                      Deprecated: use RequestID=Preserve instead
+                      Deprecated: use RequestID=PreserveOrGenerate instead
                     type: boolean
                   requestID:
                     description: |-
                       RequestID configures Envoy's behavior for handling the `X-Request-ID` header.
-                      Defaults to `Generate` and builds the `X-Request-ID` for every request and ignores pre-existing values from the edge.
+                      When omitted default behavior is `Generate` which builds the `X-Request-ID` for every request
+                       and ignores pre-existing values from the edge.
                       (An "edge request" refers to a request from an external client to the Envoy entrypoint.)
                     enum:
                     - PreserveOrGenerate
diff --git a/go.mod b/go.mod
index b9e21b1ea..fafae13a4 100644
--- a/go.mod
+++ b/go.mod
@@ -15,9 +15,9 @@ require (
 	github.com/docker/cli v28.5.1+incompatible
 	github.com/docker/docker v28.3.3+incompatible
 	github.com/dominikbraun/graph v0.23.0
-	github.com/envoyproxy/go-control-plane v0.13.5-0.20250929230642-07d3df27ff4f
+	github.com/envoyproxy/go-control-plane v0.13.5-0.20251022160057-de4316c523b7
 	github.com/envoyproxy/go-control-plane/contrib v1.32.5-0.20250430092421-68a532e11403
-	github.com/envoyproxy/go-control-plane/envoy v1.35.1-0.20250929230642-07d3df27ff4f
+	github.com/envoyproxy/go-control-plane/envoy v1.35.1-0.20251022160057-de4316c523b7
 	github.com/envoyproxy/go-control-plane/ratelimit v0.1.1-0.20250805143705-d51f8590a549
 	github.com/envoyproxy/ratelimit v1.4.1-0.20230427142404-e2a87f41d3a7
 	github.com/evanphx/json-patch v5.9.11+incompatible
@@ -42,7 +42,7 @@ require (
 	github.com/prometheus/client_model v0.6.2
 	github.com/prometheus/common v0.67.1
 	github.com/quic-go/quic-go v0.55.0
-	github.com/replicatedhq/troubleshoot v0.123.12
+	github.com/replicatedhq/troubleshoot v0.123.11
 	github.com/shopspring/decimal v1.4.0
 	github.com/spf13/cobra v1.10.1
 	github.com/spf13/pflag v1.0.10
diff --git a/go.sum b/go.sum
index e6b8f4b58..e7d2de4e0 100644
--- a/go.sum
+++ b/go.sum
@@ -173,12 +173,12 @@ github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRr
 github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
-github.com/envoyproxy/go-control-plane v0.13.5-0.20250929230642-07d3df27ff4f h1:36vvJBe/wXWfD7qrTb1WnbPVPMxNFDfEygztH8wgebw=
-github.com/envoyproxy/go-control-plane v0.13.5-0.20250929230642-07d3df27ff4f/go.mod h1:PTY7yDlLxB4bW7rEOO7e79uTDr9yXzpuI1QGIDfxEzc=
+github.com/envoyproxy/go-control-plane v0.13.5-0.20251022160057-de4316c523b7 h1:JsOVlgacLOjgYWGb4V2E/d/c/o4+HEvYjr0+KnL4j4o=
+github.com/envoyproxy/go-control-plane v0.13.5-0.20251022160057-de4316c523b7/go.mod h1:Alz8LEClvR7xKsrq3qzoc4N0guvVNSS8KmSChGYr9hs=
 github.com/envoyproxy/go-control-plane/contrib v1.32.5-0.20250430092421-68a532e11403 h1:5wPocL1bGYhA4TtKZwcdVI5fsXo1JatkbcxPBcFQswc=
 github.com/envoyproxy/go-control-plane/contrib v1.32.5-0.20250430092421-68a532e11403/go.mod h1:Xkwx/TGvEKRCL2mitdiuQWOD1ECvfM5krWWVo2vI2Zk=
-github.com/envoyproxy/go-control-plane/envoy v1.35.1-0.20250929230642-07d3df27ff4f h1:4efYrIQgVRwCmwCveby6ck+VpxqzibdOL1Out1rJqqc=
-github.com/envoyproxy/go-control-plane/envoy v1.35.1-0.20250929230642-07d3df27ff4f/go.mod h1:2LcmvJoXsDSrsGZIxGM0Gah9ykiwTn/kgjyQdnNH8Jc=
+github.com/envoyproxy/go-control-plane/envoy v1.35.1-0.20251022160057-de4316c523b7 h1:Q4zISHdb9brRNvzmQl8Bwvap7GAtGrQjHyyw9OYmkFk=
+github.com/envoyproxy/go-control-plane/envoy v1.35.1-0.20251022160057-de4316c523b7/go.mod h1:ty89S1YCCVruQAm9OtKeEkQLTb+Lkz0k8v9W0Oxsv98=
 github.com/envoyproxy/go-control-plane/ratelimit v0.1.1-0.20250805143705-d51f8590a549 h1:5K0vH5H4dtCIO8+w/yq6vDaMcGn9RoPrHfmPAFAztwU=
 github.com/envoyproxy/go-control-plane/ratelimit v0.1.1-0.20250805143705-d51f8590a549/go.mod h1:KxtyvDAPIEkqUUvF9ooo5gSGVOtQ08wUTnQe5LsJC6c=
 github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
@@ -562,8 +562,8 @@ github.com/redis/go-redis/extra/redisotel/v9 v9.0.5 h1:EfpWLLCyXw8PSM2/XNJLjI3Pb
 github.com/redis/go-redis/extra/redisotel/v9 v9.0.5/go.mod h1:WZjPDy7VNzn77AAfnAfVjZNvfJTYfPetfZk5yoSTLaQ=
 github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM=
 github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA=
-github.com/replicatedhq/troubleshoot v0.123.12 h1:XbgZJMSwIHyf1lvxIRNwI9AVsRzcA7N3AWLPLSkrr+w=
-github.com/replicatedhq/troubleshoot v0.123.12/go.mod h1:CKPCj8si77XuSL6sIAFdqtO23/eha159eEBlQF8HpVw=
+github.com/replicatedhq/troubleshoot v0.123.11 h1:gHzjBuoYrYD9NsnLNb7/AA/V5Xwn8hl0wobB/PD9aTY=
+github.com/replicatedhq/troubleshoot v0.123.11/go.mod h1:CKPCj8si77XuSL6sIAFdqtO23/eha159eEBlQF8HpVw=
 github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
 github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
 github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
diff --git a/internal/gatewayapi/backendtrafficpolicy.go b/internal/gatewayapi/backendtrafficpolicy.go
index 801dbefcd..9d3a20809 100644
--- a/internal/gatewayapi/backendtrafficpolicy.go
+++ b/internal/gatewayapi/backendtrafficpolicy.go
@@ -9,7 +9,6 @@ import (
 	"errors"
 	"fmt"
 	"math"
-	"sort"
 	"strconv"
 	"strings"
 	"time"
@@ -65,52 +64,59 @@ func (t *Translator) ProcessBackendTrafficPolicies(resources *resource.Resources
 		gatewayMap[key] = &policyGatewayTargetContext{GatewayContext: gw}
 	}
 
-	// Map of Gateway to the routes attached to it
-	gatewayRouteMap := make(map[string]sets.Set[string], gatewayMapSize)
+	// Map of Gateway to the routes attached to it.
+	gatewayRouteMap := &GatewayPolicyRouteMap{
+		Routes:       make(map[NamespacedNameWithSection]sets.Set[string], gatewayMapSize),
+		SectionIndex: make(map[types.NamespacedName]sets.Set[string], gatewayMapSize),
+	}
 
-	handledPolicies := make(map[types.NamespacedName]*egv1a1.BackendTrafficPolicy, policyMapSize)
+	// Map of attached Policy to Gateway. It is used to merge policies process.
+	gatewayPolicyMap := make(map[NamespacedNameWithSection]*egv1a1.BackendTrafficPolicy, gatewayMapSize)
+
+	// Map of Gateway to the routes merged to it.
+	gatewayPolicyMerged := &GatewayPolicyRouteMap{
+		Routes:       make(map[NamespacedNameWithSection]sets.Set[string], gatewayMapSize),
+		SectionIndex: make(map[types.NamespacedName]sets.Set[string], gatewayMapSize),
+	}
 
-	gatewayPolicyMap := make(map[types.NamespacedName]*egv1a1.BackendTrafficPolicy, gatewayMapSize)
-	gatewayPolicyMerged := make(map[types.NamespacedName]sets.Set[string], gatewayMapSize)
+	handledPolicies := make(map[types.NamespacedName]*egv1a1.BackendTrafficPolicy, policyMapSize)
 
 	// Translate
-	// 1. First translate Policies targeting xRoutes
-	// 2. Finally, the policies targeting Gateways
+	// 1. First translate Policies targeting RouteRules
+	// 2. Next translate Policies targeting xRoutes
+	// 3. Then translate Policies targeting Listeners
+	// 4. Finally, the policies targeting Gateways
+
+	// Build gateway policy maps, which are needed when processing the policies targeting xRoutes.
+	t.buildGatewayPolicyMap(backendTrafficPolicies, gateways, gatewayMap, gatewayPolicyMap)
 
-	// Build gateway policy map, which is needed when processing the policies targeting xRoutes.
-	// TODO: This loop is similar to the one 'Process the policies targeting Gateways', we may want to
-	// merge them into one if possible.
+	// Process the policies targeting RouteRules
 	for _, currPolicy := range backendTrafficPolicies {
-		targetRefs := getPolicyTargetRefs(currPolicy.Spec.PolicyTargetReferences, gateways, currPolicy.Namespace)
+		policyName := utils.NamespacedName(currPolicy)
+		targetRefs := getPolicyTargetRefs(currPolicy.Spec.PolicyTargetReferences, routes, currPolicy.Namespace)
 		for _, currTarget := range targetRefs {
-			if currTarget.Kind == resource.KindGateway {
-				// Check if the gateway exists
-				key := types.NamespacedName{
-					Name:      string(currTarget.Name),
-					Namespace: currPolicy.Namespace,
-				}
-				gateway, ok := gatewayMap[key]
-				if !ok {
-					continue
+			// If the target is not a gateway, then it's an xRoute. If the section name is defined, then it's a route rule.
+			if currTarget.Kind != resource.KindGateway && currTarget.SectionName != nil {
+				policy, found := handledPolicies[policyName]
+				if !found {
+					policy = currPolicy
+					handledPolicies[policyName] = policy
+					res = append(res, policy)
 				}
 
-				// Check if policy for gateway exists
-				gatewayNN := utils.NamespacedName(gateway)
-				_, policyExists := gatewayPolicyMap[gatewayNN]
-				if policyExists {
-					continue
-				}
-				gatewayPolicyMap[gatewayNN] = currPolicy
+				t.processBTPolicyForRoute(resources, xdsIR,
+					routeMap, gatewayRouteMap, gatewayPolicyMerged, gatewayPolicyMap, policy, currTarget)
 			}
 		}
 	}
 
-	// Process the policies targeting xRoutes
+	// Process the policies targeting Routes
 	for _, currPolicy := range backendTrafficPolicies {
 		policyName := utils.NamespacedName(currPolicy)
 		targetRefs := getPolicyTargetRefs(currPolicy.Spec.PolicyTargetReferences, routes, currPolicy.Namespace)
 		for _, currTarget := range targetRefs {
-			if currTarget.Kind != resource.KindGateway {
+			// If the target is not a gateway, then it's an xRoute. If the section name is not defined, then it's a route.
+			if currTarget.Kind != resource.KindGateway && currTarget.SectionName == nil {
 				policy, found := handledPolicies[policyName]
 				if !found {
 					policy = currPolicy
@@ -118,118 +124,27 @@ func (t *Translator) ProcessBackendTrafficPolicies(resources *resource.Resources
 					res = append(res, policy)
 				}
 
-				// Negative statuses have already been assigned so it's safe to skip
-				route, resolveErr := resolveBTPolicyRouteTargetRef(policy, currTarget, routeMap)
-				if route == nil {
-					continue
-				}
-
-				// Find the Gateway that the route belongs to and add it to the
-				// gatewayRouteMap and ancestor list, which will be used to check
-				// policy overrides and populate its ancestor status.
-				parentRefs := GetParentReferences(route)
-				ancestorRefs := make([]*gwapiv1.ParentReference, 0, len(parentRefs))
-				routeParents := sets.New[types.NamespacedName]()
-				for _, p := range parentRefs {
-					if p.Kind == nil || *p.Kind == resource.KindGateway {
-						namespace := route.GetNamespace()
-						if p.Namespace != nil {
-							namespace = string(*p.Namespace)
-						}
-						gwNN := types.NamespacedName{
-							Namespace: namespace,
-							Name:      string(p.Name),
-						}
-						routeParents.Insert(gwNN)
-
-						key := gwNN.String()
-						if _, ok := gatewayRouteMap[key]; !ok {
-							gatewayRouteMap[key] = make(sets.Set[string])
-						}
-						gatewayRouteMap[key].Insert(utils.NamespacedName(route).String())
-
-						// Do need a section name since the policy is targeting to a route
-						ancestorRef := getAncestorRefForPolicy(gwNN, p.SectionName)
-						ancestorRefs = append(ancestorRefs, &ancestorRef)
-					}
-				}
-
-				// Set conditions for resolve error, then skip current xRoute
-				if resolveErr != nil {
-					status.SetResolveErrorForPolicyAncestors(&policy.Status,
-						ancestorRefs,
-						t.GatewayControllerName,
-						policy.Generation,
-						resolveErr,
-					)
-
-					continue
-				}
-
-				if policy.Spec.MergeType == nil {
-					// Set conditions for translation error if it got any
-					if err := t.translateBackendTrafficPolicyForRoute(policy, route, xdsIR, resources, nil); err != nil {
-						status.SetTranslationErrorForPolicyAncestors(&policy.Status,
-							ancestorRefs,
-							t.GatewayControllerName,
-							policy.Generation,
-							status.Error2ConditionMsg(err),
-						)
-					}
-				} else {
-					// merge with parent target policy if exists
-					for _, gwNN := range routeParents.UnsortedList() {
-						ancestorRef := getAncestorRefForPolicy(gwNN, nil)
-						// find policy for Gateway
-						gwPolicy := gatewayPolicyMap[gwNN]
-						if gwPolicy == nil {
-							// not found, fall back to the current policy
-							if err := t.translateBackendTrafficPolicyForRoute(policy, route, xdsIR, resources, &gwNN); err != nil {
-								status.SetConditionForPolicyAncestor(&policy.Status,
-									&ancestorRef,
-									t.GatewayControllerName,
-									gwapiv1.PolicyConditionAccepted, metav1.ConditionFalse,
-									egv1a1.PolicyReasonInvalid,
-									status.Error2ConditionMsg(err),
-									policy.Generation,
-								)
-							}
-
-							continue
-						}
-
-						// merge with parent policy
-						if err := t.translateBackendTrafficPolicyForRouteWithMerge(policy, gwNN, gwPolicy, route, xdsIR, resources); err != nil {
-							status.SetConditionForPolicyAncestor(&policy.Status,
-								&ancestorRef,
-								t.GatewayControllerName,
-								gwapiv1.PolicyConditionAccepted, metav1.ConditionFalse,
-								egv1a1.PolicyReasonInvalid,
-								status.Error2ConditionMsg(err),
-								policy.Generation,
-							)
-							continue
-						}
-
-						if _, ok := gatewayPolicyMerged[gwNN]; !ok {
-							gatewayPolicyMerged[gwNN] = make(sets.Set[string])
-						}
-						gatewayPolicyMerged[gwNN].Insert(utils.NamespacedName(route).String())
+				t.processBTPolicyForRoute(resources, xdsIR,
+					routeMap, gatewayRouteMap, gatewayPolicyMerged, gatewayPolicyMap, policy, currTarget)
+			}
+		}
+	}
 
-						status.SetConditionForPolicyAncestor(&policy.Status,
-							&ancestorRef,
-							t.GatewayControllerName,
-							egv1a1.PolicyConditionMerged,
-							metav1.ConditionTrue,
-							egv1a1.PolicyReasonMerged,
-							fmt.Sprintf("Merged with policy %s/%s", gwPolicy.Namespace, gwPolicy.Name),
-							policy.Generation,
-						)
-					}
+	// Process the policies targeting Listeners
+	for _, currPolicy := range backendTrafficPolicies {
+		policyName := utils.NamespacedName(currPolicy)
+		targetRefs := getPolicyTargetRefs(currPolicy.Spec.PolicyTargetReferences, gateways, currPolicy.Namespace)
+		for _, currTarget := range targetRefs {
+			// If the target is a gateway and the section name is defined, then it's a listener.
+			if currTarget.Kind == resource.KindGateway && currTarget.SectionName != nil {
+				policy, found := handledPolicies[policyName]
+				if !found {
+					policy = currPolicy
+					handledPolicies[policyName] = policy
+					res = append(res, policy)
 				}
-
-				// Set Accepted condition if it is unset
-				status.SetAcceptedForPolicyAncestors(&policy.Status, ancestorRefs, t.GatewayControllerName, policy.Generation)
+				t.processBTPolicyForGateway(resources, xdsIR,
+					gatewayMap, gatewayRouteMap, gatewayPolicyMerged, policy, currTarget)
 			}
 		}
 	}
@@ -239,105 +154,344 @@ func (t *Translator) ProcessBackendTrafficPolicies(resources *resource.Resources
 		policyName := utils.NamespacedName(currPolicy)
 		targetRefs := getPolicyTargetRefs(currPolicy.Spec.PolicyTargetReferences, gateways, currPolicy.Namespace)
 		for _, currTarget := range targetRefs {
-			if currTarget.Kind == resource.KindGateway {
+			// If the target is a gateway and the section name is not defined, then it's a gateway.
+			if currTarget.Kind == resource.KindGateway && currTarget.SectionName == nil {
 				policy, found := handledPolicies[policyName]
 				if !found {
 					policy = currPolicy
 					handledPolicies[policyName] = policy
 					res = append(res, policy)
 				}
+				t.processBTPolicyForGateway(resources, xdsIR,
+					gatewayMap, gatewayRouteMap, gatewayPolicyMerged, policy, currTarget)
+			}
+		}
+	}
+
+	for _, policy := range res {
+		// Truncate Ancestor list of longer than 16
+		if len(policy.Status.Ancestors) > 16 {
+			status.TruncatePolicyAncestors(&policy.Status, t.GatewayControllerName, policy.Generation)
+		}
+	}
+	return res
+}
 
-				// Negative statuses have already been assigned so it's safe to skip
-				gateway, resolveErr := resolveBTPolicyGatewayTargetRef(policy, currTarget, gatewayMap)
-				if gateway == nil {
+func (t *Translator) buildGatewayPolicyMap(
+	backendTrafficPolicies []*egv1a1.BackendTrafficPolicy,
+	gateways []*GatewayContext,
+	gatewayMap map[types.NamespacedName]*policyGatewayTargetContext,
+	gatewayPolicyMap map[NamespacedNameWithSection]*egv1a1.BackendTrafficPolicy,
+) {
+	for _, currPolicy := range backendTrafficPolicies {
+		targetRefs := getPolicyTargetRefs(currPolicy.Spec.PolicyTargetReferences, gateways, currPolicy.Namespace)
+		for _, currTarget := range targetRefs {
+			if currTarget.Kind == resource.KindGateway {
+				// Check if the gateway exists
+				key := types.NamespacedName{
+					Name:      string(currTarget.Name),
+					Namespace: currPolicy.Namespace,
+				}
+				gateway, ok := gatewayMap[key]
+				if !ok {
 					continue
 				}
 
-				// Find its ancestor reference by resolved gateway, even with resolve error
-				gatewayNN := utils.NamespacedName(gateway)
-				ancestorRef := getAncestorRefForPolicy(gatewayNN, nil)
-
-				// Set conditions for resolve error, then skip current gateway
-				if resolveErr != nil {
-					status.SetResolveErrorForPolicyAncestor(&policy.Status,
-						&ancestorRef,
-						t.GatewayControllerName,
-						policy.Generation,
-						resolveErr,
-					)
+				// Check if the specified listener exists when sectionName is set
+				if currTarget.SectionName != nil {
+					if err := validateGatewayListenerSectionName(
+						*currTarget.SectionName,
+						key,
+						gateway.listeners,
+					); err != nil {
+						continue
+					}
+				}
 
+				mapKey := NamespacedNameWithSection{
+					NamespacedName: key,
+					SectionName:    ptr.Deref(currTarget.SectionName, ""),
+				}
+				if _, ok := gatewayPolicyMap[mapKey]; ok {
 					continue
 				}
+				gatewayPolicyMap[mapKey] = currPolicy
+			}
+		}
+	}
+}
 
-				// Set conditions for translation error if it got any
-				if err := t.translateBackendTrafficPolicyForGateway(policy, currTarget, gateway, xdsIR, resources); err != nil {
-					status.SetTranslationErrorForPolicyAncestor(&policy.Status,
-						&ancestorRef,
-						t.GatewayControllerName,
-						policy.Generation,
-						status.Error2ConditionMsg(err),
-					)
-				}
+func (t *Translator) processBTPolicyForRoute(
+	resources *resource.Resources,
+	xdsIR resource.XdsIRMap,
+	routeMap map[policyTargetRouteKey]*policyRouteTargetContext,
+	gatewayRouteMap *GatewayPolicyRouteMap,
+	gatewayPolicyMergedMap *GatewayPolicyRouteMap,
+	gatewayPolicyMap map[NamespacedNameWithSection]*egv1a1.BackendTrafficPolicy,
+	policy *egv1a1.BackendTrafficPolicy,
+	currTarget gwapiv1.LocalPolicyTargetReferenceWithSectionName,
+) {
+	var (
+		targetedRoute RouteContext
+		resolveErr    *status.PolicyResolveError
+	)
 
-				// Set Accepted condition if it is unset
-				status.SetAcceptedForPolicyAncestor(&policy.Status, &ancestorRef, t.GatewayControllerName, policy.Generation)
+	targetedRoute, resolveErr = resolveBTPolicyRouteTargetRef(policy, currTarget, routeMap)
+	// Skip if the route is not found
+	// It's not necessarily an error because the BackendTrafficPolicy may be
+	// reconciled by multiple controllers. And the other controller may
+	// have the target route.
+	if targetedRoute == nil {
+		return
+	}
+
+	// Find the Gateway that the route belongs to and add it to the
+	// gatewayRouteMap and ancestor list, which will be used to check
+	// policy overrides and populate its ancestor status.
+	parentRefs := GetParentReferences(targetedRoute)
+	ancestorRefs := make([]*gwapiv1.ParentReference, 0, len(parentRefs))
+	// parentRefCtxs holds parent gateway/listener contexts for using in policy merge logic.
+	parentRefCtxs := make([]*RouteParentContext, 0, len(parentRefs))
+	for _, p := range parentRefs {
+		if p.Kind == nil || *p.Kind == resource.KindGateway {
+			namespace := targetedRoute.GetNamespace()
+			if p.Namespace != nil {
+				namespace = string(*p.Namespace)
+			}
 
-				// Check if this policy is overridden by other policies targeting at
-				// route level
-				mergedRoutes, routeMerged := gatewayPolicyMerged[gatewayNN]
-				if routeMerged && len(mergedRoutes) > 0 {
-					// if merge happened, skip the overridden check
-					gatewayMergedRoutes := mergedRoutes.UnsortedList()
-					sort.Strings(gatewayMergedRoutes)
+			mapKey := NamespacedNameWithSection{
+				NamespacedName: types.NamespacedName{
+					Name:      string(p.Name),
+					Namespace: namespace,
+				},
+				SectionName: ptr.Deref(p.SectionName, ""),
+			}
+			if _, ok := gatewayRouteMap.Routes[mapKey]; !ok {
+				gatewayRouteMap.Routes[mapKey] = make(sets.Set[string])
+			}
+			gatewayRouteMap.Routes[mapKey].Insert(utils.NamespacedName(targetedRoute).String())
 
-					status.SetConditionForPolicyAncestor(&policy.Status,
-						&ancestorRef,
-						t.GatewayControllerName,
-						egv1a1.PolicyConditionMerged,
-						metav1.ConditionTrue,
-						egv1a1.PolicyReasonMerged,
-						fmt.Sprintf("This policy is being merged by other backendTrafficPolicies for these routes: %v", gatewayMergedRoutes),
-						policy.Generation,
-					)
+			// Register section name to Gateway index for efficient lookup when retrieving overridden and merged targets
+			if _, ok := gatewayRouteMap.SectionIndex[mapKey.NamespacedName]; !ok {
+				gatewayRouteMap.SectionIndex[mapKey.NamespacedName] = make(sets.Set[string])
+			}
+			gatewayRouteMap.SectionIndex[mapKey.NamespacedName].Insert(string(mapKey.SectionName))
+
+			// Do need a section name since the policy is targeting to a route.
+			ancestorRef := getAncestorRefForPolicy(mapKey.NamespacedName, p.SectionName)
+			ancestorRefs = append(ancestorRefs, &ancestorRef)
+
+			parentRefCtxs = append(parentRefCtxs, GetRouteParentContext(targetedRoute, p, t.GatewayControllerName))
+		}
+	}
+
+	// Set conditions for resolve error, then skip current xroute
+	if resolveErr != nil {
+		status.SetResolveErrorForPolicyAncestors(&policy.Status,
+			ancestorRefs,
+			t.GatewayControllerName,
+			policy.Generation,
+			resolveErr,
+		)
+		return
+	}
+
+	if policy.Spec.MergeType == nil {
+		// Set conditions for translation error if it got any
+		if err := t.translateBackendTrafficPolicyForRoute(policy, targetedRoute, currTarget, xdsIR, resources, nil, nil); err != nil {
+			status.SetTranslationErrorForPolicyAncestors(&policy.Status,
+				ancestorRefs,
+				t.GatewayControllerName,
+				policy.Generation,
+				status.Error2ConditionMsg(err),
+			)
+		}
+	} else {
+		for _, parentRefCtx := range parentRefCtxs {
+			for _, listener := range parentRefCtx.listeners {
+				gwNN := utils.NamespacedName(listener.gateway.Gateway)
+				ancestorRef := getAncestorRefForPolicy(gwNN, &listener.Name)
+
+				// Find Gateway listener level policy
+				listenerMapKey := NamespacedNameWithSection{
+					NamespacedName: gwNN,
+					SectionName:    listener.Name,
 				}
+				listenerPolicy := gatewayPolicyMap[listenerMapKey]
 
-				if r, ok := gatewayRouteMap[gatewayNN.String()]; ok {
-					// Maintain order here to ensure status/string does not change with the same data
-					var gatewayRoutes []string
-					if len(mergedRoutes) > 0 {
-						gatewayRoutes = r.Difference(mergedRoutes).UnsortedList()
-					} else {
-						gatewayRoutes = r.UnsortedList()
-					}
-					sort.Strings(gatewayRoutes)
-					if len(gatewayRoutes) == 0 {
-						continue
+				// Find Gateway level policy
+				gwMapKey := NamespacedNameWithSection{
+					NamespacedName: gwNN,
+				}
+				gwPolicy := gatewayPolicyMap[gwMapKey]
+				if gwPolicy == nil && listenerPolicy == nil {
+					// not found, fall back to the current policy
+					if err := t.translateBackendTrafficPolicyForRoute(policy, targetedRoute, currTarget, xdsIR, resources, &gwNN, &listener.Name); err != nil {
+						status.SetConditionForPolicyAncestor(&policy.Status,
+							&ancestorRef,
+							t.GatewayControllerName,
+							gwapiv1.PolicyConditionAccepted, metav1.ConditionFalse,
+							egv1a1.PolicyReasonInvalid,
+							status.Error2ConditionMsg(err),
+							policy.Generation,
+						)
 					}
+					continue
+				}
 
+				parentPolicy := gwPolicy
+				if listenerPolicy != nil {
+					parentPolicy = listenerPolicy
+				}
+				// merge with parent policy
+				if err := t.translateBackendTrafficPolicyForRouteWithMerge(
+					policy, parentPolicy, currTarget, gwNN, &listener.Name,
+					targetedRoute, xdsIR, resources,
+				); err != nil {
 					status.SetConditionForPolicyAncestor(&policy.Status,
 						&ancestorRef,
 						t.GatewayControllerName,
-						egv1a1.PolicyConditionOverridden,
-						metav1.ConditionTrue,
-						egv1a1.PolicyReasonOverridden,
-						fmt.Sprintf("This policy is being overridden by other backendTrafficPolicies for these routes: %v", gatewayRoutes),
+						gwapiv1.PolicyConditionAccepted, metav1.ConditionFalse,
+						egv1a1.PolicyReasonInvalid,
+						status.Error2ConditionMsg(err),
 						policy.Generation,
 					)
+					continue
+				}
+
+				// Record the merged routes for gateway
+				if _, ok := gatewayPolicyMergedMap.Routes[listenerMapKey]; !ok {
+					gatewayPolicyMergedMap.Routes[listenerMapKey] = make(sets.Set[string])
 				}
+				gatewayPolicyMergedMap.Routes[listenerMapKey].Insert(utils.NamespacedName(targetedRoute).String())
+
+				// Register section name to Gateway index for efficient lookup when retrieving overridden and merged targets
+				if _, ok := gatewayPolicyMergedMap.SectionIndex[listenerMapKey.NamespacedName]; !ok {
+					gatewayPolicyMergedMap.SectionIndex[listenerMapKey.NamespacedName] = make(sets.Set[string])
+				}
+				gatewayPolicyMergedMap.SectionIndex[listenerMapKey.NamespacedName].Insert(string(listenerMapKey.SectionName))
+
+				status.SetConditionForPolicyAncestor(&policy.Status,
+					&ancestorRef,
+					t.GatewayControllerName,
+					egv1a1.PolicyConditionMerged,
+					metav1.ConditionTrue,
+					egv1a1.PolicyReasonMerged,
+					fmt.Sprintf("Merged with policy %s/%s", parentPolicy.Namespace, parentPolicy.Name),
+					policy.Generation,
+				)
 			}
 		}
 	}
 
-	for _, policy := range res {
-		// Truncate Ancestor list of longer than 16
-		if len(policy.Status.Ancestors) > 16 {
-			status.TruncatePolicyAncestors(&policy.Status, t.GatewayControllerName, policy.Generation)
-		}
+	// Set Accepted condition if it is unset
+	status.SetAcceptedForPolicyAncestors(&policy.Status, ancestorRefs, t.GatewayControllerName, policy.Generation)
+
+	// Check if this policy is overridden by other policies targeting at route rule levels
+	// If policy target is route rule, we can skip the check
+	if currTarget.SectionName != nil {
+		return
+	}
+
+	key := policyTargetRouteKey{
+		Kind:      string(currTarget.Kind),
+		Name:      string(currTarget.Name),
+		Namespace: policy.Namespace,
+	}
+	overriddenTargetsMessage := getOverriddenTargetsMessageForRoute(routeMap[key], currTarget.SectionName)
+	if overriddenTargetsMessage != "" {
+		status.SetConditionForPolicyAncestors(&policy.Status,
+			ancestorRefs,
+			t.GatewayControllerName,
+			egv1a1.PolicyConditionOverridden,
+			metav1.ConditionTrue,
+			egv1a1.PolicyReasonOverridden,
+			"This policy is being overridden by other backendTrafficPolicy for "+overriddenTargetsMessage,
+			policy.Generation,
+		)
 	}
-	return res
 }
 
-func resolveBTPolicyGatewayTargetRef(policy *egv1a1.BackendTrafficPolicy, target gwapiv1.LocalPolicyTargetReferenceWithSectionName, gateways map[types.NamespacedName]*policyGatewayTargetContext) (*GatewayContext, *status.PolicyResolveError) {
+func (t *Translator) processBTPolicyForGateway(
+	resources *resource.Resources,
+	xdsIR resource.XdsIRMap,
+	gatewayMap map[types.NamespacedName]*policyGatewayTargetContext,
+	gatewayRouteMap *GatewayPolicyRouteMap,
+	gatewayPolicyMergedMap *GatewayPolicyRouteMap,
+	policy *egv1a1.BackendTrafficPolicy,
+	currTarget gwapiv1.LocalPolicyTargetReferenceWithSectionName,
+) {
+	var (
+		targetedGateway *GatewayContext
+		resolveErr      *status.PolicyResolveError
+	)
+
+	// Negative statuses have already been assigned so it's safe to skip
+	targetedGateway, resolveErr = resolveBTPolicyGatewayTargetRef(policy, currTarget, gatewayMap)
+	if targetedGateway == nil {
+		return
+	}
+
+	// Find its ancestor reference by resolved gateway, even with resolve error
+	gatewayNN := utils.NamespacedName(targetedGateway)
+	ancestorRef := getAncestorRefForPolicy(gatewayNN, currTarget.SectionName)
+
+	// Set conditions for resolve error, then skip current gateway
+	if resolveErr != nil {
+		status.SetResolveErrorForPolicyAncestor(&policy.Status,
+			&ancestorRef,
+			t.GatewayControllerName,
+			policy.Generation,
+			resolveErr,
+		)
+		return
+	}
+
+	// Set conditions for translation error if it got any
+	if err := t.translateBackendTrafficPolicyForGateway(policy, currTarget, targetedGateway, xdsIR, resources); err != nil {
+		status.SetTranslationErrorForPolicyAncestor(&policy.Status,
+			&ancestorRef,
+			t.GatewayControllerName,
+			policy.Generation,
+			status.Error2ConditionMsg(err),
+		)
+	}
+
+	// Set Accepted condition if it is unset
+	status.SetAcceptedForPolicyAncestor(&policy.Status, &ancestorRef, t.GatewayControllerName, policy.Generation)
+
+	overriddenMessage, mergedMessage := getOverriddenAndMergedTargetsMessageForGateway(
+		gatewayMap[gatewayNN], gatewayRouteMap, gatewayPolicyMergedMap, currTarget.SectionName)
+
+	if mergedMessage != "" {
+		status.SetConditionForPolicyAncestor(&policy.Status,
+			&ancestorRef,
+			t.GatewayControllerName,
+			egv1a1.PolicyConditionMerged,
+			metav1.ConditionTrue,
+			egv1a1.PolicyReasonMerged,
+			"This policy is being merged by other backendTrafficPolicies for "+mergedMessage,
+			policy.Generation,
+		)
+	}
+	if overriddenMessage != "" {
+		status.SetConditionForPolicyAncestor(&policy.Status,
+			&ancestorRef,
+			t.GatewayControllerName,
+			egv1a1.PolicyConditionOverridden,
+			metav1.ConditionTrue,
+			egv1a1.PolicyReasonOverridden,
+			"This policy is being overridden by other backendTrafficPolicies for "+overriddenMessage,
+			policy.Generation,
+		)
+	}
+}
+
+func resolveBTPolicyGatewayTargetRef(
+	policy *egv1a1.BackendTrafficPolicy,
+	target gwapiv1.LocalPolicyTargetReferenceWithSectionName,
+	gateways map[types.NamespacedName]*policyGatewayTargetContext,
+) (*GatewayContext, *status.PolicyResolveError) {
 	// Check if the gateway exists
 	key := types.NamespacedName{
 		Name:      string(target.Name),
@@ -350,25 +504,58 @@ func resolveBTPolicyGatewayTargetRef(policy *egv1a1.BackendTrafficPolicy, target
 		return nil, nil
 	}
 
-	// Check if another policy targeting the same Gateway exists
-	if gateway.attached {
-		message := fmt.Sprintf("Unable to target Gateway %s, another BackendTrafficPolicy has already attached to it",
-			string(target.Name))
+	// If sectionName is set, make sure it's valid
+	if target.SectionName != nil {
+		if err := validateGatewayListenerSectionName(
+			*target.SectionName,
+			key,
+			gateway.listeners,
+		); err != nil {
+			return gateway.GatewayContext, err
+		}
+	}
+
+	if target.SectionName == nil {
+		// Check if another policy targeting the same Gateway exists
+		if gateway.attached {
+			message := fmt.Sprintf("Unable to target Gateway %s, another BackendTrafficPolicy has already attached to it",
+				string(target.Name))
 
-		return gateway.GatewayContext, &status.PolicyResolveError{
-			Reason:  gwapiv1.PolicyReasonConflicted,
-			Message: message,
+			return gateway.GatewayContext, &status.PolicyResolveError{
+				Reason:  gwapiv1.PolicyReasonConflicted,
+				Message: message,
+			}
 		}
+
+		// Set context and save
+		gateway.attached = true
+	} else {
+		listenerName := string(*target.SectionName)
+		if gateway.attachedToListeners != nil && gateway.attachedToListeners.Has(listenerName) {
+			message := fmt.Sprintf("Unable to target Listener %s/%s, another BackendTrafficPolicy has already attached to it",
+				key, listenerName)
+
+			return gateway.GatewayContext, &status.PolicyResolveError{
+				Reason:  gwapiv1.PolicyReasonConflicted,
+				Message: message,
+			}
+		}
+		if gateway.attachedToListeners == nil {
+			gateway.attachedToListeners = make(sets.Set[string])
+		}
+		gateway.attachedToListeners.Insert(listenerName)
 	}
 
-	// Set context and save
-	gateway.attached = true
 	gateways[key] = gateway
 
 	return gateway.GatewayContext, nil
 }
 
-func resolveBTPolicyRouteTargetRef(policy *egv1a1.BackendTrafficPolicy, target gwapiv1.LocalPolicyTargetReferenceWithSectionName, routes map[policyTargetRouteKey]*policyRouteTargetContext) (RouteContext, *status.PolicyResolveError) {
+func resolveBTPolicyRouteTargetRef(
+	policy *egv1a1.BackendTrafficPolicy,
+	target gwapiv1.LocalPolicyTargetReferenceWithSectionName,
+	routes map[policyTargetRouteKey]*policyRouteTargetContext,
+) (RouteContext, *status.PolicyResolveError) {
 	// Check if the route exists
 	key := policyTargetRouteKey{
 		Kind:      string(target.Kind),
@@ -382,19 +569,42 @@ func resolveBTPolicyRouteTargetRef(policy *egv1a1.BackendTrafficPolicy, target g
 		return nil, nil
 	}
 
-	// Check if another policy targeting the same xRoute exists
-	if route.attached {
-		message := fmt.Sprintf("Unable to target %s %s, another BackendTrafficPolicy has already attached to it",
-			string(target.Kind), string(target.Name))
+	// If sectionName is set, make sure it's valid
+	if target.SectionName != nil {
+		if err := validateRouteRuleSectionName(*target.SectionName, key, route); err != nil {
+			return route.RouteContext, err
+		}
+	}
+
+	if target.SectionName == nil {
+		// Check if another policy targeting the same xRoute exists
+		if route.attached {
+			message := fmt.Sprintf("Unable to target %s %s, another BackendTrafficPolicy has already attached to it",
+				string(target.Kind), string(target.Name))
 
-		return route.RouteContext, &status.PolicyResolveError{
-			Reason:  gwapiv1.PolicyReasonConflicted,
-			Message: message,
+			return route.RouteContext, &status.PolicyResolveError{
+				Reason:  gwapiv1.PolicyReasonConflicted,
+				Message: message,
+			}
 		}
+		route.attached = true
+	} else {
+		routeRuleName := string(*target.SectionName)
+		if route.attachedToRouteRules != nil && route.attachedToRouteRules.Has(routeRuleName) {
+			message := fmt.Sprintf("Unable to target RouteRule %s/%s, another BackendTrafficPolicy has already attached to it",
+				string(target.Name), routeRuleName)
+
+			return route.RouteContext, &status.PolicyResolveError{
+				Reason:  gwapiv1.PolicyReasonConflicted,
+				Message: message,
+			}
+		}
+		if route.attachedToRouteRules == nil {
+			route.attachedToRouteRules = make(sets.Set[string])
+		}
+		route.attachedToRouteRules.Insert(routeRuleName)
 	}
 
-	// Set context and save
-	route.attached = true
 	routes[key] = route
 
 	return route.RouteContext, nil
@@ -403,9 +613,11 @@ func resolveBTPolicyRouteTargetRef(policy *egv1a1.BackendTrafficPolicy, target g
 func (t *Translator) translateBackendTrafficPolicyForRoute(
 	policy *egv1a1.BackendTrafficPolicy,
 	route RouteContext,
+	target gwapiv1.LocalPolicyTargetReferenceWithSectionName,
 	xdsIR resource.XdsIRMap,
 	resources *resource.Resources,
-	gatewayNN *types.NamespacedName,
+	policyTargetGatewayNN *types.NamespacedName,
+	policyTargetListener *gwapiv1.SectionName,
 ) error {
 	tf, errs := t.buildTrafficFeatures(policy, resources)
 	if tf == nil {
@@ -416,22 +628,23 @@ func (t *Translator) translateBackendTrafficPolicyForRoute(
 	// Apply IR to all relevant routes
 	for key, x := range xdsIR {
 		// if gatewayNN is not nil, only apply to the specific gateway
-		if gatewayNN != nil && key != t.IRKey(*gatewayNN) {
+		if policyTargetGatewayNN != nil && key != t.IRKey(*policyTargetGatewayNN) {
 			// Skip if not the gateway wanted
 			continue
 		}
-		t.applyTrafficFeatureToRoute(route, tf, errs, policy, x)
+		t.applyTrafficFeatureToRoute(route, tf, errs, policy, target, x, policyTargetListener)
 	}
 
 	return errs
 }
 
 func (t *Translator) translateBackendTrafficPolicyForRouteWithMerge(
-	policy *egv1a1.BackendTrafficPolicy,
-	gatewayNN types.NamespacedName, gwPolicy *egv1a1.BackendTrafficPolicy,
-	route RouteContext, xdsIR resource.XdsIRMap, resources *resource.Resources,
+	policy, parentPolicy *egv1a1.BackendTrafficPolicy,
+	target gwapiv1.LocalPolicyTargetReferenceWithSectionName,
+	policyTargetGatewayNN types.NamespacedName, policyTargetListener *gwapiv1.SectionName, route RouteContext,
+	xdsIR resource.XdsIRMap, resources *resource.Resources,
 ) error {
-	mergedPolicy, err := mergeBackendTrafficPolicy(policy, gwPolicy)
+	mergedPolicy, err := mergeBackendTrafficPolicy(policy, parentPolicy)
 	if err != nil {
 		return fmt.Errorf("error merging policies: %w", err)
 	}
@@ -451,9 +664,8 @@ func (t *Translator) translateBackendTrafficPolicyForRouteWithMerge(
 	// 1. Both policies have rate limits - merge them
 	// 2. Only gateway policy has rate limits - preserve gateway policy's rule names
 	// 3. Only route policy has rate limits - use route policy's rule names (default behavior)
-	if policy.Spec.RateLimit != nil && gwPolicy.Spec.RateLimit != nil {
-		// Case 1: Both policies have rate limits - merge them
-		tfGW, _ := t.buildTrafficFeatures(gwPolicy, resources)
+	if policy.Spec.RateLimit != nil && parentPolicy.Spec.RateLimit != nil {
+		tfGW, _ := t.buildTrafficFeatures(parentPolicy, resources)
 		tfRoute, _ := t.buildTrafficFeatures(policy, resources)
 
 		if tfGW != nil && tfRoute != nil &&
@@ -466,9 +678,9 @@ func (t *Translator) translateBackendTrafficPolicyForRouteWithMerge(
 			// Replace the rate limit in the merged features if successful
 			tf.RateLimit = mergedRL
 		}
-	} else if policy.Spec.RateLimit == nil && gwPolicy.Spec.RateLimit != nil {
+	} else if policy.Spec.RateLimit == nil && parentPolicy.Spec.RateLimit != nil {
 		// Case 2: Only gateway policy has rate limits - preserve gateway policy's rule names
-		tfGW, _ := t.buildTrafficFeatures(gwPolicy, resources)
+		tfGW, _ := t.buildTrafficFeatures(parentPolicy, resources)
 		if tfGW != nil && tfGW.RateLimit != nil {
 			// Use the gateway policy's rate limit with its original rule names
 			tf.RateLimit = tfGW.RateLimit
@@ -476,51 +688,91 @@ func (t *Translator) translateBackendTrafficPolicyForRouteWithMerge(
 	}
 	// Case 3: Only route policy has rate limits or neither has rate limits - use default behavior (tf already built from merged policy)
 
-	x, ok := xdsIR[t.IRKey(gatewayNN)]
+	x, ok := xdsIR[t.IRKey(policyTargetGatewayNN)]
 	if !ok {
 		// should not happen.
 		return nil
 	}
-	t.applyTrafficFeatureToRoute(route, tf, errs, mergedPolicy, x)
+	t.applyTrafficFeatureToRoute(route, tf, errs, mergedPolicy, target, x, policyTargetListener)
 
 	return nil
 }
 
 func (t *Translator) applyTrafficFeatureToRoute(route RouteContext,
 	tf *ir.TrafficFeatures, errs error,
-	policy *egv1a1.BackendTrafficPolicy, x *ir.Xds,
+	policy *egv1a1.BackendTrafficPolicy,
+	target gwapiv1.LocalPolicyTargetReferenceWithSectionName,
+	x *ir.Xds,
+	policyTargetListener *gwapiv1.SectionName,
 ) {
 	prefix := irRoutePrefix(route)
 	for _, tcp := range x.TCP {
+		// if listenerName is not nil, only apply to the specific listener
+		if policyTargetListener != nil && string(*policyTargetListener) != tcp.Metadata.SectionName {
+			// Skip if not the listener wanted
+			continue
+		}
 		for _, r := range tcp.Routes {
+			// If specified the sectionName in policy target, must match route rule from ir route metadata.
+			if target.SectionName != nil && string(*target.SectionName) != r.Destination.Metadata.SectionName {
+				continue
+			}
 			if strings.HasPrefix(r.Destination.Name, prefix) {
-				r.LoadBalancer = tf.LoadBalancer
-				r.ProxyProtocol = tf.ProxyProtocol
-				r.HealthCheck = tf.HealthCheck
-				r.CircuitBreaker = tf.CircuitBreaker
-				r.TCPKeepalive = tf.TCPKeepalive
-				r.Timeout = tf.Timeout
-				r.BackendConnection = tf.BackendConnection
-				r.DNS = tf.DNS
+				// only set attributes which weren't already set by a more
+				// specific policy
+				setIfNil(&r.LoadBalancer, tf.LoadBalancer)
+				setIfNil(&r.ProxyProtocol, tf.ProxyProtocol)
+				setIfNil(&r.HealthCheck, tf.HealthCheck)
+				setIfNil(&r.CircuitBreaker, tf.CircuitBreaker)
+				setIfNil(&r.TCPKeepalive, tf.TCPKeepalive)
+				setIfNil(&r.Timeout, tf.Timeout)
+				setIfNil(&r.BackendConnection, tf.BackendConnection)
+				setIfNil(&r.DNS, tf.DNS)
 			}
 		}
 	}
 
 	for _, udp := range x.UDP {
+		// if listenerName is not nil, only apply to the specific listener
+		if policyTargetListener != nil && string(*policyTargetListener) != udp.Metadata.SectionName {
+			// Skip if not the listener wanted
+			continue
+		}
 		if udp.Route != nil {
 			r := udp.Route
+			// If specified the sectionName in policy target, must match route rule from ir route metadata.
+			if target.SectionName != nil && string(*target.SectionName) != r.Destination.Metadata.SectionName {
+				continue
+			}
 			if strings.HasPrefix(r.Destination.Name, prefix) {
-				r.LoadBalancer = tf.LoadBalancer
-				r.DNS = tf.DNS
+				// only set attributes which weren't already set by a more
+				// specific policy
+				setIfNil(&r.LoadBalancer, tf.LoadBalancer)
+				setIfNil(&r.DNS, tf.DNS)
 			}
 		}
 	}
 
 	routesWithDirectResponse := sets.New[string]()
 	for _, http := range x.HTTP {
+		// if listenerName is not nil, only apply to the specific listener
+		if policyTargetListener != nil && string(*policyTargetListener) != http.Metadata.SectionName {
+			// Skip if not the listener wanted
+			continue
+		}
 		for _, r := range http.Routes {
+			// If specified the sectionName in policy target, must match route rule from ir route metadata.
+			if target.SectionName != nil && string(*target.SectionName) != r.Destination.Metadata.SectionName {
+				continue
+			}
 			// Apply if there is a match
 			if strings.HasPrefix(r.Name, prefix) {
+				// If any of the features are already set, it means that a more specific
+				// policy (targeting xRoute rule) has already set it, so we skip it.
+				if r.Traffic != nil || r.UseClientProtocol != nil {
+					continue
+				}
+
 				if errs != nil {
 					// Return a 500 direct response
 					r.DirectResponse = &ir.CustomResponse{
@@ -688,6 +940,11 @@ func (t *Translator) translateBackendTrafficPolicyForGateway(
 			continue
 		}
 
+		// If specified the sectionName must match listenerName from ir listener metadata.
+		if target.SectionName != nil && string(*target.SectionName) != tcp.Metadata.SectionName {
+			continue
+		}
+
 		for _, r := range tcp.Routes {
 			// only set attributes which weren't already set by a more
 			// specific policy
@@ -707,6 +964,11 @@ func (t *Translator) translateBackendTrafficPolicyForGateway(
 			continue
 		}
 
+		// If specified the sectionName must match listenerName from ir listener metadata.
+		if target.SectionName != nil && string(*target.SectionName) != udp.Metadata.SectionName {
+			continue
+		}
+
 		if udp.Route == nil {
 			continue
 		}
@@ -726,12 +988,17 @@ func (t *Translator) translateBackendTrafficPolicyForGateway(
 			continue
 		}
 
+		// If specified the sectionName must match listenerName from ir listener metadata.
+		if target.SectionName != nil && string(*target.SectionName) != http.Metadata.SectionName {
+			continue
+		}
+
 		// A Policy targeting the most specific scope(xRoute) wins over a policy
 		// targeting a lesser specific scope(Gateway).
 		for _, r := range http.Routes {
 			// If any of the features are already set, it means that a more specific
-			// policy(targeting xRoute) has already set it, so we skip it.
-			if r.Traffic != nil {
+			// policy (targeting xRoute rule, xRoute, listener) has already set it, so we skip it.
+			if r.Traffic != nil || r.UseClientProtocol != nil {
 				continue
 			}
 
@@ -746,15 +1013,15 @@ func (t *Translator) translateBackendTrafficPolicyForGateway(
 
 			r.Traffic = tf.DeepCopy()
 
+			if localTo, err := buildClusterSettingsTimeout(&policy.Spec.ClusterSettings); err == nil {
+				r.Traffic.Timeout = localTo
+			}
+
 			// Update the Host field in HealthCheck, now that we have access to the Route Hostname.
 			r.Traffic.HealthCheck.SetHTTPHostIfAbsent(r.Hostname)
 
-			if ct, err := buildClusterSettingsTimeout(&policy.Spec.ClusterSettings); err == nil {
-				r.Traffic.Timeout = ct
-			}
-
 			if policy.Spec.UseClientProtocol != nil {
-				setIfNil(&r.UseClientProtocol, policy.Spec.UseClientProtocol)
+				r.UseClientProtocol = policy.Spec.UseClientProtocol
 			}
 		}
 	}
@@ -887,14 +1154,16 @@ func buildRateLimitRule(rule egv1a1.RateLimitRule) (*ir.RateLimitRule, error) {
 			Unit:     ir.RateLimitUnit(rule.Limit.Unit),
 		},
 		HeaderMatches: make([]*ir.StringMatch, 0),
+		MethodMatches: make([]*ir.StringMatch, 0),
 		Shared:        rule.Shared,
 	}
 
 	for _, match := range rule.ClientSelectors {
-		if len(match.Headers) == 0 && match.SourceCIDR == nil {
+		if len(match.Headers) == 0 && len(match.Methods) == 0 &&
+			match.Path == nil && match.SourceCIDR == nil {
 			return nil, fmt.Errorf(
 				"unable to translate rateLimit. At least one of the" +
-					" header or sourceCIDR must be specified")
+					" header or method or path or sourceCIDR must be specified")
 		}
 		for _, header := range match.Headers {
 			switch {
@@ -934,6 +1203,38 @@ func buildRateLimitRule(rule egv1a1.RateLimitRule) (*ir.RateLimitRule, error) {
 			}
 		}
 
+		for _, method := range match.Methods {
+			irRule.MethodMatches = append(irRule.MethodMatches, &ir.StringMatch{
+				Exact:  ptr.To(string(method.Value)),
+				Invert: method.Invert,
+			})
+		}
+
+		if match.Path != nil {
+			switch ptr.Deref(match.Path.Type, gwapiv1.PathMatchPathPrefix) {
+			case gwapiv1.PathMatchPathPrefix:
+				irRule.PathMatch = &ir.StringMatch{
+					Prefix: ptr.To(match.Path.Value),
+					Invert: match.Path.Invert,
+				}
+			case gwapiv1.PathMatchExact:
+				irRule.PathMatch = &ir.StringMatch{
+					Exact:  ptr.To(match.Path.Value),
+					Invert: match.Path.Invert,
+				}
+			case gwapiv1.PathMatchRegularExpression:
+				if err := regex.Validate(match.Path.Value); err != nil {
+					return nil, err
+				}
+				irRule.PathMatch = &ir.StringMatch{
+					SafeRegex: ptr.To(match.Path.Value),
+					Invert:    match.Path.Invert,
+				}
+			default:
+				return nil, fmt.Errorf("unable to translate rateLimit: invalid path type.")
+			}
+		}
+
 		if match.SourceCIDR != nil {
 			// distinct means that each IP Address within the specified Source IP CIDR is treated as a
 			// distinct client selector and uses a separate rate limit bucket/counter.
diff --git a/internal/gatewayapi/clienttrafficpolicy.go b/internal/gatewayapi/clienttrafficpolicy.go
index 319552e9a..dbb62ef6f 100644
--- a/internal/gatewayapi/clienttrafficpolicy.go
+++ b/internal/gatewayapi/clienttrafficpolicy.go
@@ -648,7 +648,7 @@ func translateListenerHeaderSettings(headerSettings *egv1a1.HeaderSettings, http
 	if headerSettings.RequestID != nil {
 		httpIR.Headers.RequestID = (*ir.RequestIDAction)(headerSettings.RequestID)
 	} else if headerSettings.PreserveXRequestID != nil && *headerSettings.PreserveXRequestID {
-		httpIR.Headers.RequestID = ptr.To(ir.RequestIDActionPreserve)
+		httpIR.Headers.RequestID = ptr.To(ir.RequestIDActionPreserveOrGenerate)
 	}
 
 	if headerSettings.XForwardedClientCert != nil {
diff --git a/internal/gatewayapi/envoyextensionpolicy.go b/internal/gatewayapi/envoyextensionpolicy.go
index 6f3632863..ec6697d62 100644
--- a/internal/gatewayapi/envoyextensionpolicy.go
+++ b/internal/gatewayapi/envoyextensionpolicy.go
@@ -76,6 +76,7 @@ func (t *Translator) ProcessEnvoyExtensionPolicies(envoyExtensionPolicies []*egv
 		policyName := utils.NamespacedName(currPolicy)
 		targetRefs := getPolicyTargetRefs(currPolicy.Spec.PolicyTargetReferences, routes, currPolicy.Namespace)
 		for _, currTarget := range targetRefs {
+			// If the target is not a gateway, then it's an xRoute. If the section name is defined, then it's a route rule.
 			if currTarget.Kind != resource.KindGateway && currTarget.SectionName != nil {
 				policy, found := handledPolicies[policyName]
 				if !found {
@@ -95,6 +96,7 @@ func (t *Translator) ProcessEnvoyExtensionPolicies(envoyExtensionPolicies []*egv
 		policyName := utils.NamespacedName(currPolicy)
 		targetRefs := getPolicyTargetRefs(currPolicy.Spec.PolicyTargetReferences, routes, currPolicy.Namespace)
 		for _, currTarget := range targetRefs {
+			// If the target is not a gateway, then it's an xRoute. If the section name is not defined, then it's a route.
 			if currTarget.Kind != resource.KindGateway && currTarget.SectionName == nil {
 				policy, found := handledPolicies[policyName]
 				if !found {
@@ -114,6 +116,7 @@ func (t *Translator) ProcessEnvoyExtensionPolicies(envoyExtensionPolicies []*egv
 		policyName := utils.NamespacedName(currPolicy)
 		targetRefs := getPolicyTargetRefs(currPolicy.Spec.PolicyTargetReferences, gateways, currPolicy.Namespace)
 		for _, currTarget := range targetRefs {
+			// If the target is a gateway and the section name is defined, then it's a listener.
 			if currTarget.Kind == resource.KindGateway && currTarget.SectionName != nil {
 				policy, found := handledPolicies[policyName]
 				if !found {
@@ -133,6 +136,7 @@ func (t *Translator) ProcessEnvoyExtensionPolicies(envoyExtensionPolicies []*egv
 		policyName := utils.NamespacedName(currPolicy)
 		targetRefs := getPolicyTargetRefs(currPolicy.Spec.PolicyTargetReferences, gateways, currPolicy.Namespace)
 		for _, currTarget := range targetRefs {
+			// If the target is a gateway and the section name is not defined, then it's a gateway.
 			if currTarget.Kind == resource.KindGateway && currTarget.SectionName == nil {
 				policy, found := handledPolicies[policyName]
 				if !found {
diff --git a/internal/gatewayapi/helpers.go b/internal/gatewayapi/helpers.go
index 117e500d5..506c36aaf 100644
--- a/internal/gatewayapi/helpers.go
+++ b/internal/gatewayapi/helpers.go
@@ -40,6 +40,11 @@ const (
 	caCertKey = "ca.crt"
 )
 
+type NamespacedNameWithSection struct {
+	types.NamespacedName
+	gwapiv1.SectionName
+}
+
 type protocolPort struct {
 	protocol gwapiv1.ProtocolType
 	port     int32
@@ -502,6 +507,17 @@ type policyGatewayTargetContext struct {
 	attachedToListeners sets.Set[string]
 }
 
+// GatewayPolicyRouteMap tracks routes attached to Gateway Listener with an index for efficient lookups
+type GatewayPolicyRouteMap struct {
+	// Routes maps Gateway Listener to attached route names
+	Routes map[NamespacedNameWithSection]sets.Set[string]
+
+	// SectionIndex: Gateway -> SectionNames index
+	// Maintains a list of all section names (listeners) per Gateway including "" (empty string) for Gateway-level entries
+	// for efficient lookup without full Routes map iteration
+	SectionIndex map[types.NamespacedName]sets.Set[string]
+}
+
 // listenersWithSameHTTPPort returns a list of the names of all other HTTP listeners
 // that would share the same filter chain as the provided listener when translated
 // to XDS
@@ -801,3 +817,112 @@ func getOverriddenTargetsMessageForGateway(
 	}
 	return ""
 }
+
+// getOverriddenAndMergedTargetsMessageForGateway generates status messages for policies
+// indicating which listeners and routes are being overridden or merged.
+func getOverriddenAndMergedTargetsMessageForGateway(
+	targetContext *policyGatewayTargetContext,
+	gatewayRouteMap *GatewayPolicyRouteMap,
+	gatewayPolicyMergedMap *GatewayPolicyRouteMap,
+	sectionName *gwapiv1.SectionName,
+) (string, string) {
+	var overrideListeners, overrideRoutes, mergedRoutes []string
+	var overrideMessage, mergedMessage string
+
+	gwNN := utils.NamespacedName(targetContext.GatewayContext)
+
+	// Get merged targets
+	if gatewayPolicyMergedMap.Routes != nil {
+		if sectionName == nil {
+			// When sectionName is nil, retrieve routes from all listeners including Gateway-level ("")
+			if gatewayPolicyMergedMap.SectionIndex != nil && gatewayPolicyMergedMap.SectionIndex[gwNN] != nil {
+				for _, listener := range gatewayPolicyMergedMap.SectionIndex[gwNN].UnsortedList() {
+					listenerKey := NamespacedNameWithSection{
+						NamespacedName: gwNN,
+						SectionName:    gwapiv1.SectionName(listener),
+					}
+					if routeSet, ok := gatewayPolicyMergedMap.Routes[listenerKey]; ok {
+						mergedRoutes = append(mergedRoutes, routeSet.UnsortedList()...)
+					}
+				}
+			}
+		} else {
+			// When sectionName is specified, retrieve routes for that specific listener
+			listenerKey := NamespacedNameWithSection{
+				NamespacedName: gwNN,
+				SectionName:    *sectionName,
+			}
+			if routeSet, ok := gatewayPolicyMergedMap.Routes[listenerKey]; ok {
+				mergedRoutes = routeSet.UnsortedList()
+			}
+			gwKey := NamespacedNameWithSection{
+				NamespacedName: gwNN,
+				SectionName:    "",
+			}
+			if routeSet, ok := gatewayPolicyMergedMap.Routes[gwKey]; ok {
+				mergedRoutes = routeSet.UnsortedList()
+			}
+		}
+	}
+
+	// Get overridden targets
+	if gatewayRouteMap.Routes != nil {
+		if sectionName == nil {
+			if targetContext != nil {
+				overrideListeners = targetContext.attachedToListeners.UnsortedList()
+			}
+			// When sectionName is nil, retrieve routes from all listeners including Gateway-level ("")
+			if gatewayRouteMap.SectionIndex != nil && gatewayRouteMap.SectionIndex[gwNN] != nil {
+				for _, listener := range gatewayRouteMap.SectionIndex[gwNN].UnsortedList() {
+					listenerKey := NamespacedNameWithSection{
+						NamespacedName: gwNN,
+						SectionName:    gwapiv1.SectionName(listener),
+					}
+					if routeSet, ok := gatewayRouteMap.Routes[listenerKey]; ok {
+						overrideRoutes = append(overrideRoutes, routeSet.UnsortedList()...)
+					}
+				}
+			}
+		} else {
+			// When sectionName is specified, retrieve routes for that specific listener
+			listenerKey := NamespacedNameWithSection{
+				NamespacedName: gwNN,
+				SectionName:    *sectionName,
+			}
+			if routeSet, ok := gatewayRouteMap.Routes[listenerKey]; ok {
+				overrideRoutes = routeSet.UnsortedList()
+			}
+			gwKey := NamespacedNameWithSection{
+				NamespacedName: gwNN,
+				SectionName:    "",
+			}
+			if routeSet, ok := gatewayRouteMap.Routes[gwKey]; ok {
+				overrideRoutes = append(overrideRoutes, routeSet.UnsortedList()...)
+			}
+		}
+	}
+
+	// Exclude merged routes from overridden routes
+	mergedRouteSet := sets.New(mergedRoutes...)
+	overrideRouteSet := sets.New(overrideRoutes...)
+	overrideRoutes = overrideRouteSet.Difference(mergedRouteSet).UnsortedList()
+
+	if len(overrideListeners) > 0 {
+		sort.Strings(overrideListeners)
+		if len(overrideRoutes) > 0 {
+			sort.Strings(overrideRoutes)
+			overrideMessage = fmt.Sprintf("these listeners: %v and these routes: %v", overrideListeners, overrideRoutes)
+		} else {
+			overrideMessage = fmt.Sprintf("these listeners: %v", overrideListeners)
+		}
+	} else if len(overrideRoutes) > 0 {
+		sort.Strings(overrideRoutes)
+		overrideMessage = fmt.Sprintf("these routes: %v", overrideRoutes)
+	}
+
+	if len(mergedRoutes) > 0 {
+		sort.Strings(mergedRoutes)
+		mergedMessage = fmt.Sprintf("these routes: %v", mergedRoutes)
+	}
+	return overrideMessage, mergedMessage
+}
diff --git a/internal/gatewayapi/route.go b/internal/gatewayapi/route.go
index 7bf2cdd9c..e85181845 100644
--- a/internal/gatewayapi/route.go
+++ b/internal/gatewayapi/route.go
@@ -623,6 +623,52 @@ func (t *Translator) processHTTPRouteRule(
 		}
 		applyHTTPFiltersContextToIRRoute(httpFiltersContext, irRoute)
 		ruleRoutes = append(ruleRoutes, irRoute)
+
+		// When using a CORS filter with method matching that excludes OPTIONS,
+		// users must explicitly specify OPTIONS method match to handle CORS preflight requests.
+		// - https://github.com/kubernetes-sigs/gateway-api/issues/3857
+		//
+		// Envoy Gateway improves user experience by implicitly creating the envoy route for CORS preflight.
+		if (httpFiltersContext != nil && httpFiltersContext.CORS != nil) &&
+			(match.Method != nil && string(*match.Method) != "OPTIONS") {
+			corsRoute := &ir.HTTPRoute{
+				Name:              irRouteName(httpRoute, ruleIdx, matchIdx) + "/cors-preflight",
+				Metadata:          routeRuleMetadata,
+				PathMatch:         irRoute.PathMatch,
+				QueryParamMatches: irRoute.QueryParamMatches,
+				CORS:              httpFiltersContext.CORS,
+			}
+
+			// Create header matches:
+			// copy original headers (excluding :method) + add CORS headers (:method=OPTIONS, origin, access-control-request-method)
+			headerMatches := make([]*ir.StringMatch, 0, len(irRoute.HeaderMatches)+2)
+			for _, headerMatch := range irRoute.HeaderMatches {
+				// Skip the original method match for CORS preflight route to avoid conflicting method requirements.
+				if headerMatch.Name == ":method" {
+					continue
+				}
+				headerMatches = append(headerMatches, headerMatch)
+			}
+
+			corsHeaders := []*ir.StringMatch{
+				{
+					Name:  ":method",
+					Exact: ptr.To("OPTIONS"),
+				},
+				{
+					Name:      "origin",
+					SafeRegex: ptr.To(".*"),
+				},
+				{
+					Name:      "access-control-request-method",
+					SafeRegex: ptr.To(".*"),
+				},
+			}
+			headerMatches = append(headerMatches, corsHeaders...)
+
+			corsRoute.HeaderMatches = headerMatches
+			ruleRoutes = append(ruleRoutes, corsRoute)
+		}
 	}
 
 	return ruleRoutes, nil
@@ -2057,24 +2103,23 @@ func getIREndpointsFromEndpointSlice(endpointSlice *discoveryv1.EndpointSlice, p
 				continue
 			}
 			conditions := endpoint.Conditions
+
 			// Unknown Serving/Terminating (nil) should fall-back to Ready, see https://pkg.go.dev/k8s.io/api/discovery/v1#EndpointConditions
+			// So drain the endpoint if:
+			// 1. Both `Terminating` and `Serving` are != null, and either `Terminating=true` or `Serving=false`
+			// 2. Or `Ready=false`
+			var draining bool
 			if conditions.Serving != nil && conditions.Terminating != nil {
-				// Check if the endpoint is serving
-				if !*conditions.Serving {
-					continue
-				}
-				// Drain the endpoint if it is being terminated
-				draining := *conditions.Terminating
-				for _, address := range endpoint.Addresses {
-					ep := ir.NewDestEndpoint(nil, address, uint32(*endpointPort.Port), draining, endpoint.Zone)
-					endpoints = append(endpoints, ep)
-				}
-			} else if conditions.Ready == nil || *conditions.Ready {
-				for _, address := range endpoint.Addresses {
-					ep := ir.NewDestEndpoint(nil, address, uint32(*endpointPort.Port), false, endpoint.Zone)
-					endpoints = append(endpoints, ep)
-				}
+				draining = *conditions.Terminating || !*conditions.Serving
+			} else {
+				draining = conditions.Ready != nil && !*conditions.Ready
 			}
+
+			for _, address := range endpoint.Addresses {
+				ep := ir.NewDestEndpoint(nil, address, uint32(*endpointPort.Port), draining, endpoint.Zone)
+				endpoints = append(endpoints, ep)
+			}
+
 		}
 	}
 
diff --git a/internal/gatewayapi/securitypolicy.go b/internal/gatewayapi/securitypolicy.go
index d379cffca..4e4c65484 100644
--- a/internal/gatewayapi/securitypolicy.go
+++ b/internal/gatewayapi/securitypolicy.go
@@ -98,6 +98,7 @@ func (t *Translator) ProcessSecurityPolicies(securityPolicies []*egv1a1.Security
 		policyName := utils.NamespacedName(currPolicy)
 		targetRefs := getPolicyTargetRefs(currPolicy.Spec.PolicyTargetReferences, routes, currPolicy.Namespace)
 		for _, currTarget := range targetRefs {
+			// If the target is not a gateway, then it's an xRoute. If the section name is defined, then it's a route rule.
 			if currTarget.Kind != resource.KindGateway && currTarget.SectionName != nil {
 				policy, found := handledPolicies[policyName]
 				if !found {
@@ -116,6 +117,7 @@ func (t *Translator) ProcessSecurityPolicies(securityPolicies []*egv1a1.Security
 		policyName := utils.NamespacedName(currPolicy)
 		targetRefs := getPolicyTargetRefs(currPolicy.Spec.PolicyTargetReferences, routes, currPolicy.Namespace)
 		for _, currTarget := range targetRefs {
+			// If the target is not a gateway, then it's an xRoute. If the section name is not defined, then it's a route.
 			if currTarget.Kind != resource.KindGateway && currTarget.SectionName == nil {
 				policy, found := handledPolicies[policyName]
 				if !found {
@@ -134,6 +136,7 @@ func (t *Translator) ProcessSecurityPolicies(securityPolicies []*egv1a1.Security
 		policyName := utils.NamespacedName(currPolicy)
 		targetRefs := getPolicyTargetRefs(currPolicy.Spec.PolicyTargetReferences, gateways, currPolicy.Namespace)
 		for _, currTarget := range targetRefs {
+			// If the target is a gateway and the section name is defined, then it's a listener.
 			if currTarget.Kind == resource.KindGateway && currTarget.SectionName != nil {
 				policy, found := handledPolicies[policyName]
 				if !found {
@@ -152,6 +155,7 @@ func (t *Translator) ProcessSecurityPolicies(securityPolicies []*egv1a1.Security
 		policyName := utils.NamespacedName(currPolicy)
 		targetRefs := getPolicyTargetRefs(currPolicy.Spec.PolicyTargetReferences, gateways, currPolicy.Namespace)
 		for _, currTarget := range targetRefs {
+			// If the target is a gateway and the section name is not defined, then it's a gateway.
 			if currTarget.Kind == resource.KindGateway && currTarget.SectionName == nil {
 				policy, found := handledPolicies[policyName]
 				if !found {
diff --git a/internal/infrastructure/kubernetes/common/resource.go b/internal/infrastructure/kubernetes/common/resource.go
new file mode 100644
index 000000000..3c70d78db
--- /dev/null
+++ b/internal/infrastructure/kubernetes/common/resource.go
@@ -0,0 +1,65 @@
+// Copyright Envoy Gateway Authors
+// SPDX-License-Identifier: Apache-2.0
+// The full text of the Apache license is available in the LICENSE file at
+// the root of the repo.
+
+package common
+
+import (
+	policyv1 "k8s.io/api/policy/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/types"
+	"k8s.io/apimachinery/pkg/util/intstr"
+
+	egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1"
+)
+
+// ResourceKind indicates the main resources of envoy-ratelimit,
+// but also the key for the uid of their ownerReference.
+const (
+	ResourceKindDeployment = "Deployment"
+)
+
+func GetPodDisruptionBudget(pdb *egv1a1.KubernetesPodDisruptionBudgetSpec,
+	selector *metav1.LabelSelector, nn *types.NamespacedName, ownerReferences []metav1.OwnerReference,
+) (*policyv1.PodDisruptionBudget, error) {
+	// If podDisruptionBudget config is nil, ignore PodDisruptionBudget.
+	if pdb == nil {
+		return nil, nil
+	}
+
+	pdbSpec := policyv1.PodDisruptionBudgetSpec{
+		Selector: selector,
+	}
+
+	switch {
+	case pdb.MinAvailable != nil:
+		pdbSpec.MinAvailable = pdb.MinAvailable
+	case pdb.MaxUnavailable != nil:
+		pdbSpec.MaxUnavailable = pdb.MaxUnavailable
+	default:
+		pdbSpec.MinAvailable = &intstr.IntOrString{Type: intstr.Int, IntVal: 0}
+	}
+
+	podDisruptionBudget := &policyv1.PodDisruptionBudget{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      nn.Name,
+			Namespace: nn.Namespace,
+			Labels:    selector.MatchLabels,
+		},
+		TypeMeta: metav1.TypeMeta{
+			APIVersion: "policy/v1",
+			Kind:       "PodDisruptionBudget",
+		},
+		Spec: pdbSpec,
+	}
+
+	podDisruptionBudget.OwnerReferences = append(podDisruptionBudget.OwnerReferences, ownerReferences...)
+
+	// apply merge patch to PodDisruptionBudget
+	podDisruptionBudget, err := pdb.ApplyMergePatch(podDisruptionBudget)
+	if err != nil {
+		return nil, err
+	}
+	return podDisruptionBudget, nil
+}
diff --git a/internal/infrastructure/kubernetes/proxy/resource_provider.go b/internal/infrastructure/kubernetes/proxy/resource_provider.go
index 29955fb57..51fd2d4fb 100644
--- a/internal/infrastructure/kubernetes/proxy/resource_provider.go
+++ b/internal/infrastructure/kubernetes/proxy/resource_provider.go
@@ -27,6 +27,7 @@ import (
 	"github.com/envoyproxy/gateway/internal/gatewayapi"
 	gwapiresource "github.com/envoyproxy/gateway/internal/gatewayapi/resource"
 	"github.com/envoyproxy/gateway/internal/infrastructure/common"
+	infracommon "github.com/envoyproxy/gateway/internal/infrastructure/kubernetes/common"
 	"github.com/envoyproxy/gateway/internal/infrastructure/kubernetes/resource"
 	"github.com/envoyproxy/gateway/internal/ir"
 	"github.com/envoyproxy/gateway/internal/utils"
@@ -134,7 +135,7 @@ func (r *ResourceRender) LabelSelector() labels.Selector {
 	return labels.SelectorFromSet(r.stableSelector().MatchLabels)
 }
 
-func (r *ResourceRender) OwnerReferences() []metav1.OwnerReference {
+func (r *ResourceRender) ownerReferences() []metav1.OwnerReference {
 	var ownerReferences []metav1.OwnerReference
 	if r.ownerReferenceUID != nil {
 		key := gwapiresource.KindGatewayClass
@@ -172,7 +173,7 @@ func (r *ResourceRender) ServiceAccount() (*corev1.ServiceAccount, error) {
 			Name:            r.serviceAccountName(),
 			Labels:          saLabels,
 			Annotations:     r.infra.GetProxyMetadata().Annotations,
-			OwnerReferences: r.OwnerReferences(),
+			OwnerReferences: r.ownerReferences(),
 		},
 	}, nil
 }
@@ -290,7 +291,7 @@ func (r *ResourceRender) Service() (*corev1.Service, error) {
 			Namespace:       r.Namespace(),
 			Labels:          svcLabels,
 			Annotations:     annotations,
-			OwnerReferences: r.OwnerReferences(),
+			OwnerReferences: r.ownerReferences(),
 		},
 		Spec: serviceSpec,
 	}
@@ -342,7 +343,7 @@ func (r *ResourceRender) ConfigMap(cert string) (*corev1.ConfigMap, error) {
 			Name:            r.Name(),
 			Labels:          cmLabels,
 			Annotations:     r.infra.GetProxyMetadata().Annotations,
-			OwnerReferences: r.OwnerReferences(),
+			OwnerReferences: r.ownerReferences(),
 		},
 		Data: data,
 	}, nil
@@ -401,7 +402,7 @@ func (r *ResourceRender) Deployment() (*appsv1.Deployment, error) {
 			Namespace:       r.Namespace(),
 			Labels:          dpLabels,
 			Annotations:     dpAnnotations,
-			OwnerReferences: r.OwnerReferences(),
+			OwnerReferences: r.ownerReferences(),
 		},
 		Spec: appsv1.DeploymentSpec{
 			Replicas: deploymentConfig.Replicas,
@@ -491,7 +492,7 @@ func (r *ResourceRender) DaemonSet() (*appsv1.DaemonSet, error) {
 			Namespace:       r.Namespace(),
 			Labels:          dsLabels,
 			Annotations:     dsAnnotations,
-			OwnerReferences: r.OwnerReferences(),
+			OwnerReferences: r.ownerReferences(),
 		},
 		Spec: appsv1.DaemonSetSpec{
 			// Daemonset's selector is immutable.
@@ -543,45 +544,13 @@ func (r *ResourceRender) PodDisruptionBudget() (*policyv1.PodDisruptionBudget, e
 		return nil, err
 	}
 
-	pdbSpec := policyv1.PodDisruptionBudgetSpec{
-		Selector: r.stableSelector(),
-	}
-	switch {
-	case pdb.MinAvailable != nil:
-		pdbSpec.MinAvailable = pdb.MinAvailable
-	case pdb.MaxUnavailable != nil:
-		pdbSpec.MaxUnavailable = pdb.MaxUnavailable
-	default:
-		pdbSpec.MinAvailable = &intstr.IntOrString{Type: intstr.Int, IntVal: 0}
-	}
-
-	podDisruptionBudget := &policyv1.PodDisruptionBudget{
-		ObjectMeta: metav1.ObjectMeta{
-			Namespace:       r.Namespace(),
-			OwnerReferences: r.OwnerReferences(),
-			Annotations:     r.infra.GetProxyMetadata().Annotations,
-			Labels:          r.stableSelector().MatchLabels,
-		},
-		TypeMeta: metav1.TypeMeta{
-			APIVersion: "policy/v1",
-			Kind:       "PodDisruptionBudget",
-		},
-		Spec: pdbSpec,
-	}
-
 	// set name
+	resourceName := r.Name()
 	if pdb.Name != nil {
-		podDisruptionBudget.Name = *pdb.Name
-	} else {
-		podDisruptionBudget.Name = r.Name()
+		resourceName = *pdb.Name
 	}
 
-	// apply merge patch to PodDisruptionBudget
-	if podDisruptionBudget, err = pdb.ApplyMergePatch(podDisruptionBudget); err != nil {
-		return nil, err
-	}
-
-	return podDisruptionBudget, nil
+	return infracommon.GetPodDisruptionBudget(pdb, r.stableSelector(), &types.NamespacedName{Name: resourceName, Namespace: r.Namespace()}, r.ownerReferences())
 }
 
 func (r *ResourceRender) HorizontalPodAutoscaler() (*autoscalingv2.HorizontalPodAutoscaler, error) {
@@ -604,7 +573,7 @@ func (r *ResourceRender) HorizontalPodAutoscaler() (*autoscalingv2.HorizontalPod
 			Namespace:       r.Namespace(),
 			Annotations:     r.infra.GetProxyMetadata().Annotations,
 			Labels:          r.stableSelector().MatchLabels,
-			OwnerReferences: r.OwnerReferences(),
+			OwnerReferences: r.ownerReferences(),
 		},
 		Spec: autoscalingv2.HorizontalPodAutoscalerSpec{
 			ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{
diff --git a/internal/infrastructure/kubernetes/ratelimit/resource_provider.go b/internal/infrastructure/kubernetes/ratelimit/resource_provider.go
index 31654b4cd..61339c761 100644
--- a/internal/infrastructure/kubernetes/ratelimit/resource_provider.go
+++ b/internal/infrastructure/kubernetes/ratelimit/resource_provider.go
@@ -21,6 +21,7 @@ import (
 	"k8s.io/utils/ptr"
 
 	egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1"
+	"github.com/envoyproxy/gateway/internal/infrastructure/kubernetes/common"
 	"github.com/envoyproxy/gateway/internal/infrastructure/kubernetes/resource"
 	"github.com/envoyproxy/gateway/internal/utils"
 )
@@ -44,6 +45,7 @@ type ResourceRender struct {
 	rateLimit           *egv1a1.RateLimit
 	rateLimitDeployment *egv1a1.KubernetesDeploymentSpec
 	rateLimitHpa        *egv1a1.KubernetesHorizontalPodAutoscalerSpec
+	rateLimitPdb        *egv1a1.KubernetesPodDisruptionBudgetSpec
 
 	// ownerReferenceUID store the uid of its owner reference.
 	ownerReferenceUID map[string]types.UID
@@ -51,13 +53,18 @@ type ResourceRender struct {
 
 // NewResourceRender returns a new ResourceRender.
 func NewResourceRender(ns string, gateway *egv1a1.EnvoyGateway, ownerReferenceUID map[string]types.UID) *ResourceRender {
-	return &ResourceRender{
+	prov := gateway.GetEnvoyGatewayProvider().GetEnvoyGatewayKubeProvider()
+
+	render := &ResourceRender{
 		namespace:           ns,
 		rateLimit:           gateway.RateLimit,
-		rateLimitDeployment: gateway.GetEnvoyGatewayProvider().GetEnvoyGatewayKubeProvider().RateLimitDeployment,
-		rateLimitHpa:        gateway.GetEnvoyGatewayProvider().GetEnvoyGatewayKubeProvider().RateLimitHpa,
+		rateLimitDeployment: prov.RateLimitDeployment,
+		rateLimitHpa:        prov.RateLimitHpa,
+		rateLimitPdb:        prov.RateLimitPDB,
 		ownerReferenceUID:   ownerReferenceUID,
 	}
+
+	return render
 }
 
 func (r *ResourceRender) Name() string {
@@ -84,7 +91,7 @@ func enablePrometheus(rl *egv1a1.RateLimit) bool {
 }
 
 // ConfigMap returns the expected rate limit ConfigMap based on the provided infra.
-func (r *ResourceRender) ConfigMap(cert string) (*corev1.ConfigMap, error) {
+func (r *ResourceRender) ConfigMap(_ string) (*corev1.ConfigMap, error) {
 	if !enablePrometheus(r.rateLimit) {
 		return nil, nil
 	}
@@ -357,5 +364,36 @@ func (r *ResourceRender) HorizontalPodAutoscaler() (*autoscalingv2.HorizontalPod
 }
 
 func (r *ResourceRender) PodDisruptionBudget() (*policyv1.PodDisruptionBudget, error) {
-	return nil, nil
+	pdb := r.rateLimitPdb
+	if pdb == nil {
+		return nil, nil
+	}
+
+	// set name
+	resourceName := r.Name()
+	if pdb.Name != nil {
+		resourceName = *pdb.Name
+	}
+
+	return common.GetPodDisruptionBudget(r.rateLimitPdb, resource.GetSelector(rateLimitLabels()), &types.NamespacedName{
+		Name:      resourceName,
+		Namespace: r.Namespace(),
+	}, r.ownerReferences())
+}
+
+func (r *ResourceRender) ownerReferences() []metav1.OwnerReference {
+	var ownerReferences []metav1.OwnerReference
+	if r.ownerReferenceUID != nil {
+		if uid, ok := r.ownerReferenceUID[ResourceKindDeployment]; ok {
+			ownerReferences = []metav1.OwnerReference{
+				{
+					Kind:       ResourceKindDeployment,
+					APIVersion: appsAPIVersion,
+					Name:       "envoy-gateway",
+					UID:        uid,
+				},
+			}
+		}
+	}
+	return ownerReferences
 }
diff --git a/internal/ir/xds.go b/internal/ir/xds.go
index 6fd5106a3..81b85696c 100644
--- a/internal/ir/xds.go
+++ b/internal/ir/xds.go
@@ -2240,6 +2240,10 @@ type LocalRateLimit struct {
 type RateLimitRule struct {
 	// HeaderMatches define the match conditions on the request headers for this route.
 	HeaderMatches []*StringMatch `json:"headerMatches" yaml:"headerMatches"`
+	// PathMatch defines the match conditions on the request path for this route.
+	PathMatch *StringMatch `json:"pathMatch,omitempty" yaml:"pathMatch,omitempty"`
+	// MethodMatches define the match conditions on the request methods for this route.
+	MethodMatches []*StringMatch `json:"methodMatches,omitempty" yaml:"methodMatches,omitempty"`
 	// CIDRMatch define the match conditions on the source IP's CIDR for this route.
 	CIDRMatch *CIDRMatch `json:"cidrMatch,omitempty" yaml:"cidrMatch,omitempty"`
 	// Limit holds the rate limit values.
@@ -2278,7 +2282,7 @@ type CIDRMatch struct {
 
 // TODO zhaohuabing: remove this function
 func (r *RateLimitRule) IsMatchSet() bool {
-	return len(r.HeaderMatches) != 0 || r.CIDRMatch != nil
+	return len(r.HeaderMatches) != 0 || r.PathMatch != nil || len(r.MethodMatches) != 0 || r.CIDRMatch != nil
 }
 
 type RateLimitUnit egv1a1.RateLimitUnit
diff --git a/internal/ir/zz_generated.deepcopy.go b/internal/ir/zz_generated.deepcopy.go
index 217ca07fc..c9b85c465 100644
--- a/internal/ir/zz_generated.deepcopy.go
+++ b/internal/ir/zz_generated.deepcopy.go
@@ -3064,6 +3064,22 @@ func (in *RateLimitRule) DeepCopyInto(out *RateLimitRule) {
 			}
 		}
 	}
+	if in.PathMatch != nil {
+		in, out := &in.PathMatch, &out.PathMatch
+		*out = new(StringMatch)
+		(*in).DeepCopyInto(*out)
+	}
+	if in.MethodMatches != nil {
+		in, out := &in.MethodMatches, &out.MethodMatches
+		*out = make([]*StringMatch, len(*in))
+		for i := range *in {
+			if (*in)[i] != nil {
+				in, out := &(*in)[i], &(*out)[i]
+				*out = new(StringMatch)
+				(*in).DeepCopyInto(*out)
+			}
+		}
+	}
 	if in.CIDRMatch != nil {
 		in, out := &in.CIDRMatch, &out.CIDRMatch
 		*out = new(CIDRMatch)
diff --git a/internal/message/watchutil.go b/internal/message/watchutil.go
index 04f13b1d6..83ef831f6 100644
--- a/internal/message/watchutil.go
+++ b/internal/message/watchutil.go
@@ -99,8 +99,41 @@ func HandleSubscription[K comparable, V any](
 	}
 	for snapshot := range subscription {
 		watchableDepth.With(meta.LabelValues()...).Record(float64(len(subscription)))
-		for _, update := range snapshot.Updates {
+
+		for _, update := range coalesceUpdates(meta.Runner, snapshot.Updates) {
 			handleWithCrashRecovery(handle, Update[K, V](update), meta, errChans)
 		}
 	}
 }
+
+// coalesceUpdates merges multiple updates for the same key into a single update,
+// preserving the latest state for each key.
+// This helps reduce redundant processing and ensures that only the most recent update per key is handled.
+func coalesceUpdates[K comparable, V any](runner string, updates []watchable.Update[K, V]) []watchable.Update[K, V] {
+	if len(updates) <= 1 {
+		return updates
+	}
+
+	seen := make(map[K]struct{}, len(updates))
+	write := len(updates) - 1
+
+	for read := len(updates) - 1; read >= 0; read-- {
+		update := updates[read]
+		if _, ok := seen[update.Key]; ok {
+			continue
+		}
+		seen[update.Key] = struct{}{}
+		updates[write] = update
+		write--
+	}
+
+	result := updates[write+1:]
+	if len(result) != len(updates) {
+		logger.WithValues("runner", runner).Info(
+			"coalesced updates",
+			"count", len(result),
+			"before", len(updates),
+		)
+	}
+	return result
+}
diff --git a/internal/xds/extensions/extensions.gen.go b/internal/xds/extensions/extensions.gen.go
index 0ef2ad09f..cf91ea812 100644
--- a/internal/xds/extensions/extensions.gen.go
+++ b/internal/xds/extensions/extensions.gen.go
@@ -76,6 +76,7 @@ import (
 	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/clusters/dns/v3"
 	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/clusters/dynamic_forward_proxy/v3"
 	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/clusters/redis/v3"
+	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/clusters/reverse_connection/v3"
 	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/common/async_files/v3"
 	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/common/aws/v3"
 	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/common/dynamic_forward_proxy/v3"
@@ -140,6 +141,7 @@ import (
 	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/kill_request/v3"
 	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/local_ratelimit/v3"
 	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/lua/v3"
+	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/mcp/v3"
 	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/oauth2/v3"
 	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/on_demand/v3"
 	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/original_src/v3"
@@ -224,6 +226,7 @@ import (
 	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/http/custom_response/local_response_policy/v3"
 	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/http/custom_response/redirect_policy/v3"
 	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/http/early_header_mutation/header_mutation/v3"
+	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/http/ext_proc/processing_request_modifiers/mapped_attribute_builder/v3"
 	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/http/ext_proc/response_processors/save_processing_response/v3"
 	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/http/header_formatters/preserve_case/v3"
 	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/http/header_validators/envoy_default/v3"
@@ -253,6 +256,7 @@ import (
 	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/matching/common_inputs/environment_variable/v3"
 	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/matching/common_inputs/network/v3"
 	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/matching/common_inputs/ssl/v3"
+	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/matching/common_inputs/stats/v3"
 	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/matching/input_matchers/consistent_hashing/v3"
 	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/matching/input_matchers/ip/v3"
 	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/matching/input_matchers/metadata/v3"
diff --git a/internal/xds/translator/extauth.go b/internal/xds/translator/extauth.go
index 3fb3ce4cc..00f3ec871 100644
--- a/internal/xds/translator/extauth.go
+++ b/internal/xds/translator/extauth.go
@@ -134,11 +134,23 @@ func extAuthConfig(extAuth *ir.ExtAuth) (*extauthv3.ExtAuthz, error) {
 		timeout = durationpb.New(extAuth.Timeout.Duration)
 	}
 
+	var rp *corev3.RetryPolicy
+	// Set the retry policy if it exists.
+	if extAuth.Traffic != nil && extAuth.Traffic.Retry != nil {
+		var err error
+		rp, err = buildNonRouteRetryPolicy(extAuth.Traffic.Retry)
+		if err != nil {
+			return nil, fmt.Errorf("build retry policy for http service: %w", err)
+		}
+	}
+
 	if extAuth.HTTP != nil {
+		hs := httpService(extAuth.HTTP, timeout)
+		hs.RetryPolicy = rp
+
 		config.Services = &extauthv3.ExtAuthz_HttpService{
-			HttpService: httpService(extAuth.HTTP, timeout),
+			HttpService: hs,
 		}
-		// Retry policy is not supported for HTTP service.
 	} else if extAuth.GRPC != nil {
 		service := &corev3.GrpcService{
 			TargetSpecifier: &corev3.GrpcService_EnvoyGrpc_{
@@ -146,14 +158,8 @@ func extAuthConfig(extAuth *ir.ExtAuth) (*extauthv3.ExtAuthz, error) {
 			},
 			Timeout: timeout,
 		}
-		// Set the retry policy if it exists.
-		if extAuth.Traffic != nil && extAuth.Traffic.Retry != nil {
-			rp, err := buildNonRouteRetryPolicy(extAuth.Traffic.Retry)
-			if err != nil {
-				return nil, fmt.Errorf("build retry policy for gRPC service: %w", err)
-			}
-			service.RetryPolicy = rp
-		}
+		service.RetryPolicy = rp
+
 		config.Services = &extauthv3.ExtAuthz_GrpcService{
 			GrpcService: service,
 		}
diff --git a/internal/xds/translator/local_ratelimit.go b/internal/xds/translator/local_ratelimit.go
index 7a7d16ab1..369712e20 100644
--- a/internal/xds/translator/local_ratelimit.go
+++ b/internal/xds/translator/local_ratelimit.go
@@ -203,123 +203,231 @@ func buildRouteLocalRateLimits(local *ir.LocalRateLimit) (
 
 	// Rules are ORed
 	for rIdx, rule := range local.Rules {
-		var rlActions []*routev3.RateLimit_Action
-		var descriptorEntries []*rlv3.RateLimitDescriptor_Entry
-
-		// HeaderMatches
-		for mIdx, match := range rule.HeaderMatches {
-			var action *routev3.RateLimit_Action
-			var entry *rlv3.RateLimitDescriptor_Entry
-
-			if match.Distinct {
-				// For distinct matches, we only check if the header exists using the RequestHeaders action.
-				descriptorKey := getRouteRuleDescriptor(rIdx, mIdx)
-				action = &routev3.RateLimit_Action{
-					ActionSpecifier: &routev3.RateLimit_Action_RequestHeaders_{
-						RequestHeaders: &routev3.RateLimit_Action_RequestHeaders{
-							HeaderName:    match.Name,
-							DescriptorKey: descriptorKey,
-						},
-					},
-				}
-				// The descriptor entry value is not set for distinct matches, which means that each distinct
-				// value of the matched header will be counted separately.
-				entry = &rlv3.RateLimitDescriptor_Entry{
-					Key: descriptorKey,
-				}
-			} else {
-				// For exact matches, we check if there is an existing header with the matching value using the
-				// HeaderValueMatch action.
-				descriptorKey := getRouteRuleDescriptor(rIdx, mIdx)
-				descriptorVal := getRouteRuleDescriptor(rIdx, mIdx)
-				headerMatcher := &routev3.HeaderMatcher{
-					Name: match.Name,
-					HeaderMatchSpecifier: &routev3.HeaderMatcher_StringMatch{
-						StringMatch: buildXdsStringMatcher(match),
-					},
-				}
-				expectMatch := true
-				if match.Invert != nil && *match.Invert {
-					expectMatch = false
-				}
-				action = &routev3.RateLimit_Action{
-					ActionSpecifier: &routev3.RateLimit_Action_HeaderValueMatch_{
-						HeaderValueMatch: &routev3.RateLimit_Action_HeaderValueMatch{
-							DescriptorKey:   descriptorKey,
-							DescriptorValue: descriptorVal,
-							ExpectMatch: &wrapperspb.BoolValue{
-								Value: expectMatch,
-							},
-							Headers: []*routev3.HeaderMatcher{headerMatcher},
-						},
+		// If method matches specified, create one rate limit rule per method (OR behavior),
+		// these rules share the same limit counter, so they share the same descriptor.
+		methodMatches := rule.MethodMatches
+		if len(methodMatches) == 0 {
+			// Use nil to indicate no method match
+			methodMatches = []*ir.StringMatch{nil}
+		}
+
+		for _, methodMatch := range methodMatches {
+			var rlActions []*routev3.RateLimit_Action
+			var descriptorEntries []*rlv3.RateLimitDescriptor_Entry
+
+			// Build all match conditions
+			// - HeaderMatch
+			// - MethodMatch
+			// - PathMatch
+			// - CIDRMatch
+			buildHeaderMatchLocalRateLimitActions(&rlActions, &descriptorEntries, rIdx, rule.HeaderMatches)
+			buildMethodMatchLocalRateLimitAction(&rlActions, &descriptorEntries, rIdx, methodMatch)
+			buildPathMatchLocalRateLimitAction(&rlActions, &descriptorEntries, rIdx, rule.PathMatch)
+			buildCIDRMatchLocalRateLimitActions(&rlActions, &descriptorEntries, rule.CIDRMatch)
+
+			// Create rate limit and descriptor
+			rateLimits = append(rateLimits, &routev3.RateLimit{Actions: rlActions})
+			descriptors = append(descriptors, &rlv3.LocalRateLimitDescriptor{
+				Entries: descriptorEntries,
+				TokenBucket: &typev3.TokenBucket{
+					MaxTokens: uint32(rule.Limit.Requests),
+					TokensPerFill: &wrapperspb.UInt32Value{
+						Value: uint32(rule.Limit.Requests),
 					},
-				}
-				// For exact matches, the descriptor entry value is set to the generated descriptor value.
-				entry = &rlv3.RateLimitDescriptor_Entry{
-					Key:   descriptorKey,
-					Value: descriptorVal,
-				}
-			}
-			rlActions = append(rlActions, action)
-			descriptorEntries = append(descriptorEntries, entry)
+					FillInterval: ratelimit.UnitToDuration(rule.Limit.Unit),
+				},
+			})
 		}
+	}
 
-		// Source IP CIDRMatch
-		if rule.CIDRMatch != nil {
-			// For CIDR matches, we first need to check if the source IP matches the CIDR range using
-			// the MaskedRemoteAddress action.
-			mra := &routev3.RateLimit_Action_MaskedRemoteAddress{}
-			maskLen := &wrapperspb.UInt32Value{Value: rule.CIDRMatch.MaskLen}
-			if rule.CIDRMatch.IsIPv6 {
-				mra.V6PrefixMaskLen = maskLen
-			} else {
-				mra.V4PrefixMaskLen = maskLen
-			}
-			action := &routev3.RateLimit_Action{
-				ActionSpecifier: &routev3.RateLimit_Action_MaskedRemoteAddress_{
-					MaskedRemoteAddress: mra,
+	return rateLimits, descriptors
+}
+
+func buildHeaderMatchLocalRateLimitActions(
+	rlActions *[]*routev3.RateLimit_Action,
+	descriptorEntries *[]*rlv3.RateLimitDescriptor_Entry,
+	ruleIdx int,
+	headerMatches []*ir.StringMatch,
+) {
+	for mIdx, match := range headerMatches {
+		var action *routev3.RateLimit_Action
+		var entry *rlv3.RateLimitDescriptor_Entry
+
+		if match.Distinct {
+			// For distinct matches, we only check if the header exists using the RequestHeaders action.
+			descriptorKey := getRouteRuleDescriptor(ruleIdx, mIdx)
+			action = &routev3.RateLimit_Action{
+				ActionSpecifier: &routev3.RateLimit_Action_RequestHeaders_{
+					RequestHeaders: &routev3.RateLimit_Action_RequestHeaders{
+						HeaderName:    match.Name,
+						DescriptorKey: descriptorKey,
+					},
 				},
 			}
-			entry := &rlv3.RateLimitDescriptor_Entry{
-				Key:   descriptorMaskedRemoteAddress,
-				Value: rule.CIDRMatch.CIDR,
+			// The descriptor entry value is not set for distinct matches, which means that each distinct
+			// value of the matched header will be counted separately.
+			entry = &rlv3.RateLimitDescriptor_Entry{
+				Key: descriptorKey,
+			}
+		} else {
+			// For exact matches, we check if there is an existing header with the matching value using the
+			// HeaderValueMatch action.
+			descriptorKey := getRouteRuleDescriptor(ruleIdx, mIdx)
+			descriptorVal := getRouteRuleDescriptor(ruleIdx, mIdx)
+			headerMatcher := &routev3.HeaderMatcher{
+				Name: match.Name,
+				HeaderMatchSpecifier: &routev3.HeaderMatcher_StringMatch{
+					StringMatch: buildXdsStringMatcher(match),
+				},
 			}
-			descriptorEntries = append(descriptorEntries, entry)
-			rlActions = append(rlActions, action)
-
-			if rule.CIDRMatch.Distinct {
-				// If the CIDRMatch is distinct, we also need to use the RemoteAddress action to get the client IP.
-				action = &routev3.RateLimit_Action{
-					ActionSpecifier: &routev3.RateLimit_Action_RemoteAddress_{
-						RemoteAddress: &routev3.RateLimit_Action_RemoteAddress{},
+			expectMatch := match.Invert == nil || !*match.Invert
+			action = &routev3.RateLimit_Action{
+				ActionSpecifier: &routev3.RateLimit_Action_HeaderValueMatch_{
+					HeaderValueMatch: &routev3.RateLimit_Action_HeaderValueMatch{
+						DescriptorKey:   descriptorKey,
+						DescriptorValue: descriptorVal,
+						ExpectMatch: &wrapperspb.BoolValue{
+							Value: expectMatch,
+						},
+						Headers: []*routev3.HeaderMatcher{headerMatcher},
 					},
-				}
-
-				// If the CIDRMatch is distinct, we use the built-in remote address descriptor key without a value.
-				// This means that each distinct client IP will be counted separately.
-				entry = &rlv3.RateLimitDescriptor_Entry{
-					Key: descriptorRemoteAddress,
-				}
-				descriptorEntries = append(descriptorEntries, entry)
-				rlActions = append(rlActions, action)
+				},
+			}
+			// For exact matches, the descriptor entry value is set to the generated descriptor value.
+			entry = &rlv3.RateLimitDescriptor_Entry{
+				Key:   descriptorKey,
+				Value: descriptorVal,
 			}
 		}
+		*rlActions = append(*rlActions, action)
+		*descriptorEntries = append(*descriptorEntries, entry)
+	}
+}
 
-		rateLimit := &routev3.RateLimit{Actions: rlActions}
-		rateLimits = append(rateLimits, rateLimit)
+func buildPathMatchLocalRateLimitAction(
+	rlActions *[]*routev3.RateLimit_Action,
+	descriptorEntries *[]*rlv3.RateLimitDescriptor_Entry,
+	ruleIdx int,
+	pathMatch *ir.StringMatch,
+) {
+	if pathMatch == nil {
+		return
+	}
 
-		descriptor := &rlv3.LocalRateLimitDescriptor{
-			Entries: descriptorEntries,
-			TokenBucket: &typev3.TokenBucket{
-				MaxTokens: uint32(rule.Limit.Requests),
-				TokensPerFill: &wrapperspb.UInt32Value{
-					Value: uint32(rule.Limit.Requests),
+	descriptorKey := getRouteRulePathDescriptor(ruleIdx)
+	descriptorVal := getRouteRulePathDescriptor(ruleIdx)
+	headerMatcher := &routev3.HeaderMatcher{
+		Name: ":path",
+		HeaderMatchSpecifier: &routev3.HeaderMatcher_StringMatch{
+			StringMatch: buildXdsStringMatcher(pathMatch),
+		},
+	}
+	expectMatch := pathMatch.Invert == nil || !*pathMatch.Invert
+	action := &routev3.RateLimit_Action{
+		ActionSpecifier: &routev3.RateLimit_Action_HeaderValueMatch_{
+			HeaderValueMatch: &routev3.RateLimit_Action_HeaderValueMatch{
+				DescriptorKey:   descriptorKey,
+				DescriptorValue: descriptorVal,
+				ExpectMatch: &wrapperspb.BoolValue{
+					Value: expectMatch,
 				},
-				FillInterval: ratelimit.UnitToDuration(rule.Limit.Unit),
+				Headers: []*routev3.HeaderMatcher{headerMatcher},
 			},
+		},
+	}
+	entry := &rlv3.RateLimitDescriptor_Entry{
+		Key:   descriptorKey,
+		Value: descriptorVal,
+	}
+
+	*rlActions = append(*rlActions, action)
+	*descriptorEntries = append(*descriptorEntries, entry)
+}
+
+func buildCIDRMatchLocalRateLimitActions(
+	rlActions *[]*routev3.RateLimit_Action,
+	descriptorEntries *[]*rlv3.RateLimitDescriptor_Entry,
+	cidrMatch *ir.CIDRMatch,
+) {
+	if cidrMatch == nil {
+		return
+	}
+
+	// For CIDR matches, we first need to check if the source IP matches the CIDR range using
+	// the MaskedRemoteAddress action.
+	mra := &routev3.RateLimit_Action_MaskedRemoteAddress{}
+	maskLen := &wrapperspb.UInt32Value{Value: cidrMatch.MaskLen}
+	if cidrMatch.IsIPv6 {
+		mra.V6PrefixMaskLen = maskLen
+	} else {
+		mra.V4PrefixMaskLen = maskLen
+	}
+	action := &routev3.RateLimit_Action{
+		ActionSpecifier: &routev3.RateLimit_Action_MaskedRemoteAddress_{
+			MaskedRemoteAddress: mra,
+		},
+	}
+	entry := &rlv3.RateLimitDescriptor_Entry{
+		Key:   descriptorMaskedRemoteAddress,
+		Value: cidrMatch.CIDR,
+	}
+	*descriptorEntries = append(*descriptorEntries, entry)
+	*rlActions = append(*rlActions, action)
+
+	if cidrMatch.Distinct {
+		// If the CIDRMatch is distinct, we also need to use the RemoteAddress action to get the client IP.
+		action = &routev3.RateLimit_Action{
+			ActionSpecifier: &routev3.RateLimit_Action_RemoteAddress_{
+				RemoteAddress: &routev3.RateLimit_Action_RemoteAddress{},
+			},
+		}
+
+		// If the CIDRMatch is distinct, we use the built-in remote address descriptor key without a value.
+		// This means that each distinct client IP will be counted separately.
+		entry = &rlv3.RateLimitDescriptor_Entry{
+			Key: descriptorRemoteAddress,
 		}
-		descriptors = append(descriptors, descriptor)
+		*descriptorEntries = append(*descriptorEntries, entry)
+		*rlActions = append(*rlActions, action)
 	}
+}
 
-	return rateLimits, descriptors
+func buildMethodMatchLocalRateLimitAction(
+	rlActions *[]*routev3.RateLimit_Action,
+	descriptorEntries *[]*rlv3.RateLimitDescriptor_Entry,
+	ruleIdx int,
+	methodMatch *ir.StringMatch,
+) {
+	if methodMatch == nil {
+		return
+	}
+
+	descriptorKey := getRouteRuleMethodDescriptor(ruleIdx)
+	descriptorVal := getRouteRuleMethodDescriptor(ruleIdx)
+	headerMatcher := &routev3.HeaderMatcher{
+		Name: ":method",
+		HeaderMatchSpecifier: &routev3.HeaderMatcher_StringMatch{
+			StringMatch: buildXdsStringMatcher(methodMatch),
+		},
+	}
+	expectMatch := methodMatch.Invert == nil || !*methodMatch.Invert
+	action := &routev3.RateLimit_Action{
+		ActionSpecifier: &routev3.RateLimit_Action_HeaderValueMatch_{
+			HeaderValueMatch: &routev3.RateLimit_Action_HeaderValueMatch{
+				DescriptorKey:   descriptorKey,
+				DescriptorValue: descriptorVal,
+				ExpectMatch: &wrapperspb.BoolValue{
+					Value: expectMatch,
+				},
+				Headers: []*routev3.HeaderMatcher{headerMatcher},
+			},
+		},
+	}
+	entry := &rlv3.RateLimitDescriptor_Entry{
+		Key:   descriptorKey,
+		Value: descriptorVal,
+	}
+
+	*rlActions = append(*rlActions, action)
+	*descriptorEntries = append(*descriptorEntries, entry)
 }
diff --git a/internal/xds/translator/ratelimit.go b/internal/xds/translator/ratelimit.go
index be4570948..54257c48e 100644
--- a/internal/xds/translator/ratelimit.go
+++ b/internal/xds/translator/ratelimit.go
@@ -213,164 +213,263 @@ func buildRouteRateLimits(route *ir.HTTPRoute) (rateLimits []*routev3.RateLimit,
 
 	// Iterate over each rule in the global rate limit configuration.
 	for rIdx, rule := range global.Rules {
-		// Create a list of rate limit actions for the current rule.
-		var rlActions []*routev3.RateLimit_Action
-
-		// Create the route descriptor using the rule's shared attribute
-		var descriptorKey, descriptorValue string
-		if isRuleShared(rule) {
-			// For shared rule, use full rule name
-			descriptorKey = rule.Name
-			descriptorValue = rule.Name
-		} else {
-			// For non-shared rule, use route name in descriptor
-			descriptorKey = getRouteDescriptor(route.Name)
-			descriptorValue = descriptorKey
+		// If method matches specified, create one rate limit rule per method (OR behavior),
+		// these rules share the same limit counter, so they share the same descriptor.
+		methodMatches := rule.MethodMatches
+		if len(methodMatches) == 0 {
+			// Use nil to indicate no method match
+			methodMatches = []*ir.StringMatch{nil}
 		}
 
-		// Create a generic key action for the route descriptor.
-		routeDescriptor := &routev3.RateLimit_Action{
-			ActionSpecifier: &routev3.RateLimit_Action_GenericKey_{
-				GenericKey: &routev3.RateLimit_Action_GenericKey{
-					DescriptorKey:   descriptorKey,
-					DescriptorValue: descriptorValue,
-				},
-			},
-		}
-
-		// Add the generic key action
-		rlActions = append(rlActions, routeDescriptor)
-
-		// Calculate the domain-specific rule index (0-based for each domain)
-		ruleIsShared := isRuleShared(rule)
-		domainRuleIdx := getDomainRuleIndex(global.Rules, rIdx, ruleIsShared)
-
-		// Process each header match in the rule.
-		for mIdx, match := range rule.HeaderMatches {
-			var action *routev3.RateLimit_Action
+		for _, methodMatch := range methodMatches {
+			// Create a list of rate limit actions for the current rule.
+			var rlActions []*routev3.RateLimit_Action
 
-			// Handle distinct matches by setting up request header actions.
-			if match.Distinct {
-				descriptorKey := getRouteRuleDescriptor(domainRuleIdx, mIdx)
-				action = &routev3.RateLimit_Action{
-					ActionSpecifier: &routev3.RateLimit_Action_RequestHeaders_{
-						RequestHeaders: &routev3.RateLimit_Action_RequestHeaders{
-							HeaderName:    match.Name,
-							DescriptorKey: descriptorKey,
-						},
-					},
-				}
+			// Create the route descriptor using the rule's shared attribute
+			var descriptorKey, descriptorValue string
+			if isRuleShared(rule) {
+				// For shared rule, use full rule name
+				descriptorKey = rule.Name
+				descriptorValue = rule.Name
 			} else {
-				// Handle non-distinct matches by setting up header value match actions.
-				descriptorKey := getRouteRuleDescriptor(domainRuleIdx, mIdx)
-				descriptorVal := getRouteRuleDescriptor(domainRuleIdx, mIdx)
-				headerMatcher := &routev3.HeaderMatcher{
-					Name: match.Name,
-					HeaderMatchSpecifier: &routev3.HeaderMatcher_StringMatch{
-						StringMatch: buildXdsStringMatcher(match),
-					},
-				}
-				expectMatch := true
-				if match.Invert != nil && *match.Invert {
-					expectMatch = false
-				}
-				action = &routev3.RateLimit_Action{
-					ActionSpecifier: &routev3.RateLimit_Action_HeaderValueMatch_{
-						HeaderValueMatch: &routev3.RateLimit_Action_HeaderValueMatch{
-							DescriptorKey:   descriptorKey,
-							DescriptorValue: descriptorVal,
-							ExpectMatch: &wrapperspb.BoolValue{
-								Value: expectMatch,
-							},
-							Headers: []*routev3.HeaderMatcher{headerMatcher},
-						},
-					},
-				}
+				// For non-shared rule, use route name in descriptor
+				descriptorKey = getRouteDescriptor(route.Name)
+				descriptorValue = descriptorKey
 			}
-			// Add the action to the list of rate limit actions.
-			rlActions = append(rlActions, action)
-		}
 
-		// To be able to rate limit each individual IP, we need to use a nested descriptors structure in the configuration
-		// of the rate limit server:
-		// * the outer layer is a masked_remote_address descriptor that catches all the source IPs inside a specified CIDR.
-		// * the inner layer is a remote_address descriptor that sets the limit for individual IP.
-		//
-		// An example of rate limit server configuration looks like this:
-		//
-		//  descriptors:
-		//    - key: masked_remote_address //catch all the source IPs inside a CIDR
-		//      value: 192.168.0.0/16
-		//      descriptors:
-		//        - key: remote_address //set limit for individual IP
-		//          rate_limit:
-		//            unit: second
-		//            requests_per_unit: 100
-		//
-		// Please refer to [Rate Limit Service Descriptor list definition](https://github.com/envoyproxy/ratelimit#descriptor-list-definition) for details.
-		// If a CIDR match is specified, add MaskedRemoteAddress and RemoteAddress descriptors.
-		if rule.CIDRMatch != nil {
-			// Setup MaskedRemoteAddress action.
-			mra := &routev3.RateLimit_Action_MaskedRemoteAddress{}
-			maskLen := &wrapperspb.UInt32Value{Value: rule.CIDRMatch.MaskLen}
-			if rule.CIDRMatch.IsIPv6 {
-				mra.V6PrefixMaskLen = maskLen
-			} else {
-				mra.V4PrefixMaskLen = maskLen
-			}
-			action := &routev3.RateLimit_Action{
-				ActionSpecifier: &routev3.RateLimit_Action_MaskedRemoteAddress_{
-					MaskedRemoteAddress: mra,
+			// Create a generic key action for the route descriptor.
+			routeDescriptor := &routev3.RateLimit_Action{
+				ActionSpecifier: &routev3.RateLimit_Action_GenericKey_{
+					GenericKey: &routev3.RateLimit_Action_GenericKey{
+						DescriptorKey:   descriptorKey,
+						DescriptorValue: descriptorValue,
+					},
 				},
 			}
-			rlActions = append(rlActions, action)
 
-			// Setup RemoteAddress action if distinct match is set.
-			if rule.CIDRMatch.Distinct {
-				action = &routev3.RateLimit_Action{
-					ActionSpecifier: &routev3.RateLimit_Action_RemoteAddress_{
-						RemoteAddress: &routev3.RateLimit_Action_RemoteAddress{},
+			// Add the generic key action
+			rlActions = append(rlActions, routeDescriptor)
+
+			// Calculate the domain-specific rule index (0-based for each domain)
+			ruleIsShared := isRuleShared(rule)
+			domainRuleIdx := getDomainRuleIndex(global.Rules, rIdx, ruleIsShared)
+
+			// Process each header match in the rule.
+			buildHeaderMatchRateLimitActions(&rlActions, domainRuleIdx, rule.HeaderMatches)
+			// Process each method match in the rule.
+			buildMethodMatchRateLimitActions(&rlActions, domainRuleIdx, methodMatch)
+			// Process each path match in the rule.
+			buildPathMatchRateLimitActions(&rlActions, domainRuleIdx, rule.PathMatch)
+			// Process each CIDR match in the rule.
+			buildCIDRMatchRateLimitActions(&rlActions, rule.CIDRMatch)
+
+			// Case when both header/method/path and cidr match are not set and the ratelimit
+			// will be applied to all traffic.
+			// 3) No Match (apply to all traffic)
+			if !rule.IsMatchSet() {
+				action := &routev3.RateLimit_Action{
+					ActionSpecifier: &routev3.RateLimit_Action_GenericKey_{
+						GenericKey: &routev3.RateLimit_Action_GenericKey{
+							DescriptorKey:   getRouteRuleDescriptor(domainRuleIdx, -1),
+							DescriptorValue: getRouteRuleDescriptor(domainRuleIdx, -1),
+						},
 					},
 				}
 				rlActions = append(rlActions, action)
 			}
+
+			// Create a rate limit object for the current rule.
+			rateLimit := &routev3.RateLimit{Actions: rlActions}
+
+			if c := rule.RequestCost; c != nil {
+				// Set the hits addend for the request cost if specified.
+				rateLimit.HitsAddend = rateLimitCostToHitsAddend(c)
+				costSpecified = true
+			}
+			// Add the rate limit to the list of rate limits.
+			rateLimits = append(rateLimits, rateLimit)
+
+			// Handle response cost by creating a separate rate limit object.
+			if c := rule.ResponseCost; c != nil {
+				responseRule := &routev3.RateLimit{Actions: rlActions, ApplyOnStreamDone: true}
+				responseRule.HitsAddend = rateLimitCostToHitsAddend(c)
+				rateLimits = append(rateLimits, responseRule)
+				costSpecified = true
+			}
 		}
+	}
+	return
+}
 
-		// Case when both header and cidr match are not set and the ratelimit
-		// will be applied to all traffic.
-		// 3) No Match (apply to all traffic)
-		if !rule.IsMatchSet() {
-			action := &routev3.RateLimit_Action{
-				ActionSpecifier: &routev3.RateLimit_Action_GenericKey_{
-					GenericKey: &routev3.RateLimit_Action_GenericKey{
-						DescriptorKey:   getRouteRuleDescriptor(domainRuleIdx, -1),
-						DescriptorValue: getRouteRuleDescriptor(domainRuleIdx, -1),
+func buildHeaderMatchRateLimitActions(
+	rlActions *[]*routev3.RateLimit_Action,
+	ruleIdx int,
+	headerMatches []*ir.StringMatch,
+) {
+	for mIdx, match := range headerMatches {
+		var action *routev3.RateLimit_Action
+
+		// Handle distinct matches by setting up request header actions.
+		if match.Distinct {
+			descriptorKey := getRouteRuleDescriptor(ruleIdx, mIdx)
+			action = &routev3.RateLimit_Action{
+				ActionSpecifier: &routev3.RateLimit_Action_RequestHeaders_{
+					RequestHeaders: &routev3.RateLimit_Action_RequestHeaders{
+						HeaderName:    match.Name,
+						DescriptorKey: descriptorKey,
+					},
+				},
+			}
+		} else {
+			// Handle non-distinct matches by setting up header value match actions.
+			descriptorKey := getRouteRuleDescriptor(ruleIdx, mIdx)
+			descriptorVal := getRouteRuleDescriptor(ruleIdx, mIdx)
+			headerMatcher := &routev3.HeaderMatcher{
+				Name: match.Name,
+				HeaderMatchSpecifier: &routev3.HeaderMatcher_StringMatch{
+					StringMatch: buildXdsStringMatcher(match),
+				},
+			}
+			expectMatch := true
+			if match.Invert != nil && *match.Invert {
+				expectMatch = false
+			}
+			action = &routev3.RateLimit_Action{
+				ActionSpecifier: &routev3.RateLimit_Action_HeaderValueMatch_{
+					HeaderValueMatch: &routev3.RateLimit_Action_HeaderValueMatch{
+						DescriptorKey:   descriptorKey,
+						DescriptorValue: descriptorVal,
+						ExpectMatch: &wrapperspb.BoolValue{
+							Value: expectMatch,
+						},
+						Headers: []*routev3.HeaderMatcher{headerMatcher},
 					},
 				},
 			}
-			rlActions = append(rlActions, action)
 		}
+		// Add the action to the list of rate limit actions.
+		*rlActions = append(*rlActions, action)
+	}
+}
 
-		// Create a rate limit object for the current rule.
-		rateLimit := &routev3.RateLimit{Actions: rlActions}
+func buildMethodMatchRateLimitActions(
+	rlActions *[]*routev3.RateLimit_Action,
+	ruleIdx int,
+	methodMatch *ir.StringMatch,
+) {
+	if methodMatch == nil {
+		return
+	}
 
-		if c := rule.RequestCost; c != nil {
-			// Set the hits addend for the request cost if specified.
-			rateLimit.HitsAddend = rateLimitCostToHitsAddend(c)
-			costSpecified = true
-		}
-		// Add the rate limit to the list of rate limits.
-		rateLimits = append(rateLimits, rateLimit)
-
-		// Handle response cost by creating a separate rate limit object.
-		if c := rule.ResponseCost; c != nil {
-			responseRule := &routev3.RateLimit{Actions: rlActions, ApplyOnStreamDone: true}
-			responseRule.HitsAddend = rateLimitCostToHitsAddend(c)
-			rateLimits = append(rateLimits, responseRule)
-			costSpecified = true
+	descriptorKey := getRouteRuleMethodDescriptor(ruleIdx)
+	descriptorVal := getRouteRuleMethodDescriptor(ruleIdx)
+	headerMatcher := &routev3.HeaderMatcher{
+		Name: ":method",
+		HeaderMatchSpecifier: &routev3.HeaderMatcher_StringMatch{
+			StringMatch: buildXdsStringMatcher(methodMatch),
+		},
+	}
+	expectMatch := methodMatch.Invert == nil || !*methodMatch.Invert
+	action := &routev3.RateLimit_Action{
+		ActionSpecifier: &routev3.RateLimit_Action_HeaderValueMatch_{
+			HeaderValueMatch: &routev3.RateLimit_Action_HeaderValueMatch{
+				DescriptorKey:   descriptorKey,
+				DescriptorValue: descriptorVal,
+				ExpectMatch: &wrapperspb.BoolValue{
+					Value: expectMatch,
+				},
+				Headers: []*routev3.HeaderMatcher{headerMatcher},
+			},
+		},
+	}
+	// Add the action to the list of rate limit actions.
+	*rlActions = append(*rlActions, action)
+}
+
+func buildPathMatchRateLimitActions(
+	rlActions *[]*routev3.RateLimit_Action,
+	ruleIdx int,
+	pathMatch *ir.StringMatch,
+) {
+	if pathMatch == nil {
+		return
+	}
+
+	descriptorKey := getRouteRulePathDescriptor(ruleIdx)
+	descriptorVal := getRouteRulePathDescriptor(ruleIdx)
+	headerMatcher := &routev3.HeaderMatcher{
+		Name: ":path",
+		HeaderMatchSpecifier: &routev3.HeaderMatcher_StringMatch{
+			StringMatch: buildXdsStringMatcher(pathMatch),
+		},
+	}
+	expectMatch := pathMatch.Invert == nil || !*pathMatch.Invert
+	action := &routev3.RateLimit_Action{
+		ActionSpecifier: &routev3.RateLimit_Action_HeaderValueMatch_{
+			HeaderValueMatch: &routev3.RateLimit_Action_HeaderValueMatch{
+				DescriptorKey:   descriptorKey,
+				DescriptorValue: descriptorVal,
+				ExpectMatch: &wrapperspb.BoolValue{
+					Value: expectMatch,
+				},
+				Headers: []*routev3.HeaderMatcher{headerMatcher},
+			},
+		},
+	}
+	// Add the action to the list of rate limit actions.
+	*rlActions = append(*rlActions, action)
+}
+
+// To be able to rate limit each individual IP, we need to use a nested descriptors structure in the configuration
+// of the rate limit server:
+// * the outer layer is a masked_remote_address descriptor that catches all the source IPs inside a specified CIDR.
+// * the inner layer is a remote_address descriptor that sets the limit for individual IP.
+//
+// An example of rate limit server configuration looks like this:
+//
+//	descriptors:
+//	  - key: masked_remote_address //catch all the source IPs inside a CIDR
+//	    value: 192.168.0.0/16
+//	    descriptors:
+//	      - key: remote_address //set limit for individual IP
+//	        rate_limit:
+//	          unit: second
+//	          requests_per_unit: 100
+//
+// Please refer to [Rate Limit Service Descriptor list definition](https://github.com/envoyproxy/ratelimit#descriptor-list-definition) for details.
+// If a CIDR match is specified, add MaskedRemoteAddress and RemoteAddress descriptors.
+func buildCIDRMatchRateLimitActions(
+	rlActions *[]*routev3.RateLimit_Action,
+	cidrMatch *ir.CIDRMatch,
+) {
+	if cidrMatch == nil {
+		return
+	}
+
+	// Setup MaskedRemoteAddress action.
+	mra := &routev3.RateLimit_Action_MaskedRemoteAddress{}
+	maskLen := &wrapperspb.UInt32Value{Value: cidrMatch.MaskLen}
+	if cidrMatch.IsIPv6 {
+		mra.V6PrefixMaskLen = maskLen
+	} else {
+		mra.V4PrefixMaskLen = maskLen
+	}
+	action := &routev3.RateLimit_Action{
+		ActionSpecifier: &routev3.RateLimit_Action_MaskedRemoteAddress_{
+			MaskedRemoteAddress: mra,
+		},
+	}
+	*rlActions = append(*rlActions, action)
+
+	// Setup RemoteAddress action if distinct match is set.
+	if cidrMatch.Distinct {
+		action = &routev3.RateLimit_Action{
+			ActionSpecifier: &routev3.RateLimit_Action_RemoteAddress_{
+				RemoteAddress: &routev3.RateLimit_Action_RemoteAddress{},
+			},
 		}
+		*rlActions = append(*rlActions, action)
 	}
-	return
 }
 
 func rateLimitCostToHitsAddend(c *ir.RateLimitCost) *routev3.RateLimit_HitsAddend {
@@ -597,8 +696,10 @@ func buildRateLimitServiceDescriptors(route *ir.HTTPRoute) []*rlsconfv3.RateLimi
 	// The order in which matching descriptors are built is consistent with
 	// the order in which ratelimit actions are built:
 	//  1) Header Matches
-	//  2) CIDR Match
-	//  3) No Match
+	//  2) Method Match
+	//  3) Path Match
+	//  4) CIDR Match
+	//  5) No Match
 
 	for rIdx, rule := range global.Rules {
 		rateLimitPolicy := &rlsconfv3.RateLimitPolicy{
@@ -635,6 +736,44 @@ func buildRateLimitServiceDescriptors(route *ir.HTTPRoute) []*rlsconfv3.RateLimi
 			cur = pbDesc
 
 			// Do not add the RateLimitPolicy to the last header match descriptor yet,
+			// as it is also possible that Method/Path/CIDR match descriptor also exist.
+		}
+
+		// 2) Method Match
+		if len(rule.MethodMatches) > 0 {
+			pbDesc := new(rlsconfv3.RateLimitDescriptor)
+			pbDesc.Key = getRouteRuleMethodDescriptor(domainRuleIdx)
+			pbDesc.Value = getRouteRuleMethodDescriptor(domainRuleIdx)
+
+			if cur != nil {
+				// The header match descriptor chain exist, add current
+				// descriptor to the chain.
+				cur.Descriptors = []*rlsconfv3.RateLimitDescriptor{pbDesc}
+			} else {
+				head = pbDesc
+			}
+			cur = pbDesc
+
+			// Do not add the RateLimitPolicy to method match descriptor yet,
+			// as it is also possible that Path/CIDR match descriptor also exist.
+		}
+
+		// 3) Path Match
+		if rule.PathMatch != nil {
+			pbDesc := new(rlsconfv3.RateLimitDescriptor)
+			pbDesc.Key = getRouteRulePathDescriptor(domainRuleIdx)
+			pbDesc.Value = getRouteRulePathDescriptor(domainRuleIdx)
+
+			if cur != nil {
+				// The header/method match descriptor chain exist, add current
+				// descriptor to the chain.
+				cur.Descriptors = []*rlsconfv3.RateLimitDescriptor{pbDesc}
+			} else {
+				head = pbDesc
+			}
+			cur = pbDesc
+
+			// Do not add the RateLimitPolicy to path match descriptor yet,
 			// as it is also possible that CIDR match descriptor also exist.
 		}
 
@@ -659,7 +798,7 @@ func buildRateLimitServiceDescriptors(route *ir.HTTPRoute) []*rlsconfv3.RateLimi
 		//            requests_per_unit: 100
 		//
 		// Please refer to [Rate Limit Service Descriptor list definition](https://github.com/envoyproxy/ratelimit#descriptor-list-definition) for details.
-		// 2) CIDR Match
+		// 4) CIDR Match
 		if rule.CIDRMatch != nil {
 			// MaskedRemoteAddress case
 			pbDesc := new(rlsconfv3.RateLimitDescriptor)
@@ -667,7 +806,7 @@ func buildRateLimitServiceDescriptors(route *ir.HTTPRoute) []*rlsconfv3.RateLimi
 			pbDesc.Value = rule.CIDRMatch.CIDR
 
 			if cur != nil {
-				// The header match descriptor chain exist, add current
+				// The header/method/path match descriptor chain exist, add current
 				// descriptor to the chain.
 				cur.Descriptors = []*rlsconfv3.RateLimitDescriptor{pbDesc}
 			} else {
@@ -682,7 +821,7 @@ func buildRateLimitServiceDescriptors(route *ir.HTTPRoute) []*rlsconfv3.RateLimi
 				cur = pbDesc
 			}
 		}
-		// Case when both header and cidr match are not set and the ratelimit
+		// Case when both header/method/path and cidr match are not set and the ratelimit
 		// will be applied to all traffic.
 		// 3) No Match (apply to all traffic)
 		if !rule.IsMatchSet() {
@@ -705,6 +844,14 @@ func getRouteRuleDescriptor(ruleIndex, matchIndex int) string {
 	return "rule-" + strconv.Itoa(ruleIndex) + "-match-" + strconv.Itoa(matchIndex)
 }
 
+func getRouteRuleMethodDescriptor(ruleIndex int) string {
+	return "rule-" + strconv.Itoa(ruleIndex) + "-method"
+}
+
+func getRouteRulePathDescriptor(ruleIndex int) string {
+	return "rule-" + strconv.Itoa(ruleIndex) + "-path"
+}
+
 func getRouteDescriptor(routeName string) string {
 	return routeName
 }
diff --git a/release-notes/current.yaml b/release-notes/current.yaml
index f6a6ee2b7..3976a861d 100644
--- a/release-notes/current.yaml
+++ b/release-notes/current.yaml
@@ -28,6 +28,9 @@ new features: |
   Added support for http2 stream timeout
   Added support for Envoy PreconnectPolicy in BackendTrafficPolicy.
   Added support for binaryData in ConfigMap referenced by HTTPRouteFilter for direct response.
+  Added support PDB for Ratelimit service.
+  Added support for allowing OPTIONS method implicitly when HTTPRoute CORS filter is used.
+  Added support rate limit based on path and method in BackendTrafficPolicy.
 
 bug fixes: |
   Fixed %ROUTE_KIND% operator to be lower-cased when used by clusterStatName in EnvoyProxy API.
@@ -49,6 +52,7 @@ bug fixes: |
   Truncated Gateway API status condition messages to stay within Kubernetes limits and prevent update failures.
   Fixed an issue in EnvoyPatchPolicy where it didn't match the target Gateway/GatewayClass due to an incorrect name reference.
   Fixed certificate SAN overlap detection in gateway listeners.
+  Fixed description and translation behavior for PreserveXRequestID
 
 # Enhancements that improve performance.
 performance improvements: |
diff --git a/site/content/en/news/case-studies/SAP/SAP.md b/site/content/en/news/case-studies/SAP/SAP.md
new file mode 100644
index 000000000..a74531a4f
--- /dev/null
+++ b/site/content/en/news/case-studies/SAP/SAP.md
@@ -0,0 +1,54 @@
+---
+date: 2025-10-15
+title: "From Evaluation to Integration: SAP's Journey with Envoy Gateway"
+linkTitle: SAP
+author: Guy Daich (SAP)
+copyright: Copyright 2025, SAP  SE or an SAP affiliate company and Envoy Gateway Authors
+---
+
+## Why Envoy Gateway
+
+Our journey with Envoy Gateway (EG) began in late 2023, when multiple SAP product teams undertook an initiative to modernize and streamline their existing gateway infrastructure, which consists of various 3rd party, open source, and managed solutions running on diverse infrastructure. We decided on Envoy as the data plane and conducted a rigorous control-plane evaluation. Envoy Gateway, then [pre-GA](https://sched.co/1Rj4s) (General Availability), emerged as the strategic choice. Selecting Envoy Gateway was not a trivial decision given the number of mature control planes in the market.
+
+However, the following factors made Envoy Gateway the right strategic choice for us:
+* Strong alignment with the Envoy Proxy organization and maintainer community.
+* An [open-source-first roadmap](../../../contributions/roadmap/) that matched our requirements and balanced community and vendor needs.
+* A leading [implementation of Gateway API](https://gateway-api.sigs.k8s.io/implementations/#envoy-gateway), providing portability and long-term viability.
+* Multi-vendor backing committed to a [common layer for Envoy-based gateways](https://blog.envoyproxy.io/introducing-envoy-gateway-ad385cc59532) and announced plans to migrate to this common layer.
+* Strong [extensibility](../../../docs/tasks/extensibility/) that accelerates delivery without waiting on upstream changes.
+
+During our evaluation, we also identified capability and readiness gaps. We concluded that successful adoption required us to take a proactive role as significant contributors, committing to a sustained engineering effort to close those gaps in collaboration with the community. This was essential to meeting enterprise timelines and helping the broader ecosystem reach GA with confidence.
+
+## Driving Production Readiness
+
+Our contributions were initially guided by the project’s [GA roadmap](https://github.com/envoyproxy/gateway/issues/2249) and focused on three areas to help the project reach this critical milestone for both our adoption and the community at large:
+* Features: Extended client and backend traffic policies to cover [timeouts](../../../docs/api/extension_types/#timeout), [HTTP](../../../docs/api/extension_types/#http1settings) and [TLS](../../../docs/api/extension_types/#clienttlssettings) options, [circuit breakers](../../../docs/tasks/traffic/circuit-breaker/), [retries](../../../docs/tasks/traffic/retry/), [backend mTLS](../../..//docs/tasks/security/backend-mtls/), [failover](../../../docs/tasks/traffic/failover/), and more.
+* Reliability: Added control-plane leader election, hardened the translation pipeline with robust error handling, and implemented various resilience and upgrade tests.
+* Processes: Improved project hygiene through [image](https://github.com/envoyproxy/gateway/pull/3287), [dependency](https://github.com/envoyproxy/gateway/pull/3261), and [license](https://github.com/envoyproxy/gateway/pull/3407) scanning, and collaborated on developing a clear [security policy](https://github.com/envoyproxy/gateway/blob/main/SECURITY.md) and coordinated disclosure process.
+
+After GA, we focused on high-impact capabilities requested by the Envoy Gateway and the Gateway API communities. Using Gateway API extension points, we introduced EG-native capabilities:
+* Routing to non-Kubernetes backends (external domains, Unix domain sockets, etc.) using a custom [backend resource](../../../docs/tasks/traffic/backend/), later extended by the community for dynamic proxy use-cases.
+* Advanced route actions via an Envoy Gateway [HTTPRouteFilter](../../../docs/api/extension_types/#httproutefilter), starting with [regex rewrites](../../../docs/api/extension_types/#httpurlrewritefilter) and subsequently expanded by the community to direct responses, credential injection, and more.
+
+For scenarios beyond common API Gateway patterns, we invested in the extensibility of both the data plane and the control plane:
+* Data Plane: We co-designed and co-implemented the [Envoy Extension Policy](../../../contributions/design/envoy-extension-policy/) resource with the community, enabling features such as [Ext-Proc](../../../docs/tasks/extensibility/ext-proc/) and [Wasm](../../../docs/tasks/extensibility/wasm/), filter ordering, and per-route extensions.
+* Control Plane: We invested in the [Envoy Gateway Extension Server](../../../docs/tasks/extensibility/extension-server/), a programmable XDS mutation path. We added [Custom Policies](https://github.com/envoyproxy/gateway/issues/2975) to pass extension context via the same policy model used by the Gateway API. We strengthened [error handling](https://github.com/envoyproxy/gateway/pull/5540), [resilience](https://github.com/envoyproxy/gateway/issues/5612), and [security](https://github.com/envoyproxy/gateway/pull/5613) while exposing additional [deployment modes](https://github.com/envoyproxy/gateway/pull/3494).
+
+These extensibility options became foundational for vendors aiming to deliver distinctive features, large-scale enterprises seeking additional configuration flexibility, and domain-specific projects such as [Envoy AI Gateway](https://aigateway.envoyproxy.io/). The community continues to invest in these extensibility options, introducing [Lua](../../../docs/tasks/extensibility/lua/) extensions and [custom backend extension](../../../docs/api/extension_types/#extensionmanager) resources. 
+
+
+{{% alert title="Major Milestones" color="primary" %}}
+
+![KubeConNA24](/img/eg-kubecon-na-2024-update.png)
+
+From [Envoy Gateway Project Update](https://sched.co/1iW9c) presented at KubeCon NA 2024. SAP contributed to key feature delivery at every milestone, becoming a [leading contributor](https://envoy.devstats.cncf.io/d/4/company-statistics-by-repository-group?var-period=d7&var-metric=activity&var-repogroup_name=envoyproxy%2Fgateway&var-companies=All) to the project.
+{{% /alert %}}
+
+## Adopting Envoy Gateway
+
+In parallel, we executed a controlled rollout across diverse environments. We identified and resolved real-world resilience, scale, and performance bottlenecks along the way. The outcomes were substantial: significantly lower CPU and memory consumption, much larger configuration scale support, and markedly faster configuration programming times. Envoy Gateway now runs reliably across hundreds of clusters worldwide on multiple infrastructure providers.
+
+Envoy Gateway is currently used to manage HTTPS traffic across multiple cloud products. The project's robust support for data plane extensibility has enabled SAP to implement organization-specific policies and routing capabilities. Moreover, control plane extensibility has allowed SAP to overcome limitations in the Gateway API and fine-tune low-level Envoy configuration options that are currently not exposed, demonstrating the versatility and power of Envoy Gateway in meeting the unique demands of an enterprise environment.
+
+We are grateful to the contributors, reviewers, maintainers, and committee members who shaped this work. We’re genuinely excited to see our joint efforts in production and to watch the foundation we helped build continuously improve and be extended by end users and adopters. If you’re building on Envoy and value openness and collaboration, we invite you to join the [Envoy Gateway community](https://gateway.envoyproxy.io/).
+
diff --git a/site/content/en/news/case-studies/_index.md b/site/content/en/news/case-studies/_index.md
new file mode 100644
index 000000000..4e3135980
--- /dev/null
+++ b/site/content/en/news/case-studies/_index.md
@@ -0,0 +1,8 @@
++++
+title = "Case Studies"
+description = "Adopter Case Studies about Envoy Gateway"
+linktitle = "Case Studies"
+
+[[cascade]]
+type = "docs"
++++
diff --git a/site/static/img/eg-kubecon-na-2024-update.png b/site/static/img/eg-kubecon-na-2024-update.png
new file mode 100644
index 000000000..947c7218d
Binary files /dev/null and b/site/static/img/eg-kubecon-na-2024-update.png differ
diff --git a/tools/make/docs.mk b/tools/make/docs.mk
index ade2d8ced..8d61f08af 100644
--- a/tools/make/docs.mk
+++ b/tools/make/docs.mk
@@ -4,7 +4,7 @@ RELEASE_VERSIONS ?= $(foreach v,$(wildcard ${ROOT_DIR}/docs/*),$(notdir ${v}))
 #       find a way to remove github.com from ignore list
 # TODO: example.com is not a valid domain, we should remove it from ignore list
 # TODO: https://www.gnu.org/software/make became unstable, we should remove it from ignore list later
-LINKINATOR_IGNORE := "opentelemetry.io github.com jwt.io githubusercontent.com example.com github.io gnu.org _print canva.com sched.co sap.com httpbin.org nemlig.com verve.com developer.hashicorp.com"
+LINKINATOR_IGNORE := "opentelemetry.io github.com jwt.io githubusercontent.com example.com github.io gnu.org _print canva.com sched.co sap.com httpbin.org nemlig.com verve.com developer.hashicorp.com gateway-api.sigs.k8s.io"
 CLEAN_NODE_MODULES ?= true
 
 ##@ Docs



Test patch:
diff --git a/internal/gatewayapi/route_test.go b/internal/gatewayapi/route_test.go
index 31de02fd1..e402cfad1 100644
--- a/internal/gatewayapi/route_test.go
+++ b/internal/gatewayapi/route_test.go
@@ -176,7 +176,7 @@ func TestGetIREndpointsFromEndpointSlices(t *testing.T) {
 			expectedAddrType: ir.MIXED,
 		},
 		{
-			name: "Keep serving and terminating as draining",
+			name: "Keep non-serving or terminating as draining",
 			endpointSlices: []*discoveryv1.EndpointSlice{
 				{
 					ObjectMeta:  metav1.ObjectMeta{Name: "slice1"},
@@ -201,6 +201,8 @@ func TestGetIREndpointsFromEndpointSlices(t *testing.T) {
 			portProtocol: corev1.ProtocolTCP,
 			expectedEndpoints: []*ir.DestinationEndpoint{
 				{Host: "192.0.2.1", Port: 80, Draining: true},
+				{Host: "192.0.2.2", Port: 80, Draining: true},
+				{Host: "192.0.2.3", Port: 80, Draining: true},
 			},
 			expectedAddrType: ir.IP,
 		},
diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-compression-json-merge.out.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-compression-json-merge.out.yaml
index 6b11b2cc6..dc3123081 100644
--- a/internal/gatewayapi/testdata/backendtrafficpolicy-compression-json-merge.out.yaml
+++ b/internal/gatewayapi/testdata/backendtrafficpolicy-compression-json-merge.out.yaml
@@ -20,20 +20,13 @@ backendTrafficPolicies:
         kind: Gateway
         name: gateway-1
         namespace: envoy-gateway
+        sectionName: http
       conditions:
       - lastTransitionTime: null
         message: Merged with policy envoy-gateway/gateway-compression-policy
         reason: Merged
         status: "True"
         type: Merged
-      controllerName: gateway.envoyproxy.io/gatewayclass-controller
-    - ancestorRef:
-        group: gateway.networking.k8s.io
-        kind: Gateway
-        name: gateway-1
-        namespace: envoy-gateway
-        sectionName: http
-      conditions:
       - lastTransitionTime: null
         message: Policy has been accepted.
         reason: Accepted
diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-compression-strategic-merge.out.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-compression-strategic-merge.out.yaml
index ce50d0ebc..3497cc5b6 100644
--- a/internal/gatewayapi/testdata/backendtrafficpolicy-compression-strategic-merge.out.yaml
+++ b/internal/gatewayapi/testdata/backendtrafficpolicy-compression-strategic-merge.out.yaml
@@ -20,20 +20,13 @@ backendTrafficPolicies:
         kind: Gateway
         name: gateway-1
         namespace: envoy-gateway
+        sectionName: http
       conditions:
       - lastTransitionTime: null
         message: Merged with policy envoy-gateway/gateway-compression-policy
         reason: Merged
         status: "True"
         type: Merged
-      controllerName: gateway.envoyproxy.io/gatewayclass-controller
-    - ancestorRef:
-        group: gateway.networking.k8s.io
-        kind: Gateway
-        name: gateway-1
-        namespace: envoy-gateway
-        sectionName: http
-      conditions:
       - lastTransitionTime: null
         message: Policy has been accepted.
         reason: Accepted
@@ -86,20 +79,13 @@ backendTrafficPolicies:
         kind: Gateway
         name: gateway-1
         namespace: envoy-gateway
+        sectionName: http
       conditions:
       - lastTransitionTime: null
         message: Merged with policy envoy-gateway/gateway-compression-policy
         reason: Merged
         status: "True"
         type: Merged
-      controllerName: gateway.envoyproxy.io/gatewayclass-controller
-    - ancestorRef:
-        group: gateway.networking.k8s.io
-        kind: Gateway
-        name: gateway-1
-        namespace: envoy-gateway
-        sectionName: http
-      conditions:
       - lastTransitionTime: null
         message: Policy has been accepted.
         reason: Accepted
diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-compressor-json-merge-disable-all.out.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-compressor-json-merge-disable-all.out.yaml
index 2da2e6578..1147dd34f 100644
--- a/internal/gatewayapi/testdata/backendtrafficpolicy-compressor-json-merge-disable-all.out.yaml
+++ b/internal/gatewayapi/testdata/backendtrafficpolicy-compressor-json-merge-disable-all.out.yaml
@@ -21,20 +21,13 @@ backendTrafficPolicies:
         kind: Gateway
         name: gateway-1
         namespace: envoy-gateway
+        sectionName: http
       conditions:
       - lastTransitionTime: null
         message: Merged with policy envoy-gateway/gateway-compressor-policy
         reason: Merged
         status: "True"
         type: Merged
-      controllerName: gateway.envoyproxy.io/gatewayclass-controller
-    - ancestorRef:
-        group: gateway.networking.k8s.io
-        kind: Gateway
-        name: gateway-1
-        namespace: envoy-gateway
-        sectionName: http
-      conditions:
       - lastTransitionTime: null
         message: Policy has been accepted.
         reason: Accepted
diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-compressor-json-merge-enable-only-gzip.out.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-compressor-json-merge-enable-only-gzip.out.yaml
index a447168d9..b18c321f7 100644
--- a/internal/gatewayapi/testdata/backendtrafficpolicy-compressor-json-merge-enable-only-gzip.out.yaml
+++ b/internal/gatewayapi/testdata/backendtrafficpolicy-compressor-json-merge-enable-only-gzip.out.yaml
@@ -20,20 +20,13 @@ backendTrafficPolicies:
         kind: Gateway
         name: gateway-1
         namespace: envoy-gateway
+        sectionName: http
       conditions:
       - lastTransitionTime: null
         message: Merged with policy envoy-gateway/gateway-compressor-policy
         reason: Merged
         status: "True"
         type: Merged
-      controllerName: gateway.envoyproxy.io/gatewayclass-controller
-    - ancestorRef:
-        group: gateway.networking.k8s.io
-        kind: Gateway
-        name: gateway-1
-        namespace: envoy-gateway
-        sectionName: http
-      conditions:
       - lastTransitionTime: null
         message: Policy has been accepted.
         reason: Accepted
diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-compressor-json-merge-enable-only-zstd.out.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-compressor-json-merge-enable-only-zstd.out.yaml
index c4e1bd603..fee028e6f 100644
--- a/internal/gatewayapi/testdata/backendtrafficpolicy-compressor-json-merge-enable-only-zstd.out.yaml
+++ b/internal/gatewayapi/testdata/backendtrafficpolicy-compressor-json-merge-enable-only-zstd.out.yaml
@@ -20,20 +20,13 @@ backendTrafficPolicies:
         kind: Gateway
         name: gateway-1
         namespace: envoy-gateway
+        sectionName: http
       conditions:
       - lastTransitionTime: null
         message: Merged with policy envoy-gateway/gateway-compressor-policy
         reason: Merged
         status: "True"
         type: Merged
-      controllerName: gateway.envoyproxy.io/gatewayclass-controller
-    - ancestorRef:
-        group: gateway.networking.k8s.io
-        kind: Gateway
-        name: gateway-1
-        namespace: envoy-gateway
-        sectionName: http
-      conditions:
       - lastTransitionTime: null
         message: Policy has been accepted.
         reason: Accepted
diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-compressor-json-merge-switch-compression.out.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-compressor-json-merge-switch-compression.out.yaml
index 465d14c67..11b48a20d 100644
--- a/internal/gatewayapi/testdata/backendtrafficpolicy-compressor-json-merge-switch-compression.out.yaml
+++ b/internal/gatewayapi/testdata/backendtrafficpolicy-compressor-json-merge-switch-compression.out.yaml
@@ -23,20 +23,13 @@ backendTrafficPolicies:
         kind: Gateway
         name: gateway-1
         namespace: envoy-gateway
+        sectionName: http
       conditions:
       - lastTransitionTime: null
         message: Merged with policy envoy-gateway/gateway-compressor-policy
         reason: Merged
         status: "True"
         type: Merged
-      controllerName: gateway.envoyproxy.io/gatewayclass-controller
-    - ancestorRef:
-        group: gateway.networking.k8s.io
-        kind: Gateway
-        name: gateway-1
-        namespace: envoy-gateway
-        sectionName: http
-      conditions:
       - lastTransitionTime: null
         message: Policy has been accepted.
         reason: Accepted
diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-compressor-json-merge.out.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-compressor-json-merge.out.yaml
index 69c23d917..da6899dd9 100644
--- a/internal/gatewayapi/testdata/backendtrafficpolicy-compressor-json-merge.out.yaml
+++ b/internal/gatewayapi/testdata/backendtrafficpolicy-compressor-json-merge.out.yaml
@@ -21,20 +21,13 @@ backendTrafficPolicies:
         kind: Gateway
         name: gateway-1
         namespace: envoy-gateway
+        sectionName: http
       conditions:
       - lastTransitionTime: null
         message: Merged with policy envoy-gateway/gateway-compressor-policy
         reason: Merged
         status: "True"
         type: Merged
-      controllerName: gateway.envoyproxy.io/gatewayclass-controller
-    - ancestorRef:
-        group: gateway.networking.k8s.io
-        kind: Gateway
-        name: gateway-1
-        namespace: envoy-gateway
-        sectionName: http
-      conditions:
       - lastTransitionTime: null
         message: Policy has been accepted.
         reason: Accepted
diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-compressor-strategic-merge.out.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-compressor-strategic-merge.out.yaml
index 6c960dad6..af59cfae3 100644
--- a/internal/gatewayapi/testdata/backendtrafficpolicy-compressor-strategic-merge.out.yaml
+++ b/internal/gatewayapi/testdata/backendtrafficpolicy-compressor-strategic-merge.out.yaml
@@ -20,20 +20,13 @@ backendTrafficPolicies:
         kind: Gateway
         name: gateway-1
         namespace: envoy-gateway
+        sectionName: http
       conditions:
       - lastTransitionTime: null
         message: Merged with policy envoy-gateway/gateway-compressor-policy
         reason: Merged
         status: "True"
         type: Merged
-      controllerName: gateway.envoyproxy.io/gatewayclass-controller
-    - ancestorRef:
-        group: gateway.networking.k8s.io
-        kind: Gateway
-        name: gateway-1
-        namespace: envoy-gateway
-        sectionName: http
-      conditions:
       - lastTransitionTime: null
         message: Policy has been accepted.
         reason: Accepted
@@ -86,20 +79,13 @@ backendTrafficPolicies:
         kind: Gateway
         name: gateway-1
         namespace: envoy-gateway
+        sectionName: http
       conditions:
       - lastTransitionTime: null
         message: Merged with policy envoy-gateway/gateway-compressor-policy
         reason: Merged
         status: "True"
         type: Merged
-      controllerName: gateway.envoyproxy.io/gatewayclass-controller
-    - ancestorRef:
-        group: gateway.networking.k8s.io
-        kind: Gateway
-        name: gateway-1
-        namespace: envoy-gateway
-        sectionName: http
-      conditions:
       - lastTransitionTime: null
         message: Policy has been accepted.
         reason: Accepted
diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-invalid-no-section.in.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-invalid-no-section.in.yaml
new file mode 100644
index 000000000..2f2b45fc6
--- /dev/null
+++ b/internal/gatewayapi/testdata/backendtrafficpolicy-invalid-no-section.in.yaml
@@ -0,0 +1,65 @@
+gateways:
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: Gateway
+  metadata:
+    namespace: envoy-gateway
+    name: gateway-1
+  spec:
+    gatewayClassName: envoy-gateway-class
+    listeners:
+    - name: listener-1
+      protocol: HTTP
+      port: 8081
+      allowedRoutes:
+        namespaces:
+          from: All
+httpRoutes:
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: HTTPRoute
+  metadata:
+    namespace: default
+    name: httproute-1
+  spec:
+    parentRefs:
+    - namespace: envoy-gateway
+      name: gateway-1
+      sectionName: listener-1
+    rules:
+    - matches:
+      - path:
+          value: "/foo"
+      backendRefs:
+      - name: service-1
+        port: 8080
+backendTrafficPolicies:
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+  kind: BackendTrafficPolicy
+  metadata:
+    namespace: envoy-gateway
+    name: policy-for-gateway-1
+  spec:
+    targetRef:
+      group: gateway.networking.k8s.io
+      kind: Gateway
+      name: gateway-1
+      sectionName: not-found-listener
+    timeout:
+      tcp:
+        connectTimeout: 15s
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+  kind: BackendTrafficPolicy
+  metadata:
+    namespace: default
+    name: policy-for-route-1
+  spec:
+    targetRef:
+      group: gateway.networking.k8s.io
+      kind: HTTPRoute
+      name: httproute-1
+      sectionName: not-found-route-rule
+    timeout:
+      tcp:
+        connectTimeout: 15s
+      http:
+        connectionIdleTimeout: 16s
+        maxConnectionDuration: 17s
diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-invalid-no-section.out.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-invalid-no-section.out.yaml
new file mode 100644
index 000000000..77420bc3c
--- /dev/null
+++ b/internal/gatewayapi/testdata/backendtrafficpolicy-invalid-no-section.out.yaml
@@ -0,0 +1,233 @@
+backendTrafficPolicies:
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+  kind: BackendTrafficPolicy
+  metadata:
+    name: policy-for-route-1
+    namespace: default
+  spec:
+    targetRef:
+      group: gateway.networking.k8s.io
+      kind: HTTPRoute
+      name: httproute-1
+      sectionName: not-found-route-rule
+    timeout:
+      http:
+        connectionIdleTimeout: 16s
+        maxConnectionDuration: 17s
+      tcp:
+        connectTimeout: 15s
+  status:
+    ancestors:
+    - ancestorRef:
+        group: gateway.networking.k8s.io
+        kind: Gateway
+        name: gateway-1
+        namespace: envoy-gateway
+        sectionName: listener-1
+      conditions:
+      - lastTransitionTime: null
+        message: No section name not-found-route-rule found for HTTPRoute default/httproute-1
+        reason: TargetNotFound
+        status: "False"
+        type: Accepted
+      controllerName: gateway.envoyproxy.io/gatewayclass-controller
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+  kind: BackendTrafficPolicy
+  metadata:
+    name: policy-for-gateway-1
+    namespace: envoy-gateway
+  spec:
+    targetRef:
+      group: gateway.networking.k8s.io
+      kind: Gateway
+      name: gateway-1
+      sectionName: not-found-listener
+    timeout:
+      tcp:
+        connectTimeout: 15s
+  status:
+    ancestors:
+    - ancestorRef:
+        group: gateway.networking.k8s.io
+        kind: Gateway
+        name: gateway-1
+        namespace: envoy-gateway
+        sectionName: not-found-listener
+      conditions:
+      - lastTransitionTime: null
+        message: No section name not-found-listener found for Gateway envoy-gateway/gateway-1
+        reason: TargetNotFound
+        status: "False"
+        type: Accepted
+      controllerName: gateway.envoyproxy.io/gatewayclass-controller
+gateways:
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: Gateway
+  metadata:
+    name: gateway-1
+    namespace: envoy-gateway
+  spec:
+    gatewayClassName: envoy-gateway-class
+    listeners:
+    - allowedRoutes:
+        namespaces:
+          from: All
+      name: listener-1
+      port: 8081
+      protocol: HTTP
+  status:
+    listeners:
+    - attachedRoutes: 1
+      conditions:
+      - lastTransitionTime: null
+        message: Sending translated listener configuration to the data plane
+        reason: Programmed
+        status: "True"
+        type: Programmed
+      - lastTransitionTime: null
+        message: Listener has been successfully translated
+        reason: Accepted
+        status: "True"
+        type: Accepted
+      - lastTransitionTime: null
+        message: Listener references have been resolved
+        reason: ResolvedRefs
+        status: "True"
+        type: ResolvedRefs
+      name: listener-1
+      supportedKinds:
+      - group: gateway.networking.k8s.io
+        kind: HTTPRoute
+      - group: gateway.networking.k8s.io
+        kind: GRPCRoute
+httpRoutes:
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: HTTPRoute
+  metadata:
+    name: httproute-1
+    namespace: default
+  spec:
+    parentRefs:
+    - name: gateway-1
+      namespace: envoy-gateway
+      sectionName: listener-1
+    rules:
+    - backendRefs:
+      - name: service-1
+        port: 8080
+      matches:
+      - path:
+          value: /foo
+  status:
+    parents:
+    - conditions:
+      - lastTransitionTime: null
+        message: Route is accepted
+        reason: Accepted
+        status: "True"
+        type: Accepted
+      - lastTransitionTime: null
+        message: Resolved all the Object references for the Route
+        reason: ResolvedRefs
+        status: "True"
+        type: ResolvedRefs
+      controllerName: gateway.envoyproxy.io/gatewayclass-controller
+      parentRef:
+        name: gateway-1
+        namespace: envoy-gateway
+        sectionName: listener-1
+infraIR:
+  envoy-gateway/gateway-1:
+    proxy:
+      listeners:
+      - address: null
+        name: envoy-gateway/gateway-1/listener-1
+        ports:
+        - containerPort: 8081
+          name: http-8081
+          protocol: HTTP
+          servicePort: 8081
+      metadata:
+        labels:
+          gateway.envoyproxy.io/owning-gateway-name: gateway-1
+          gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway
+        ownerReference:
+          kind: GatewayClass
+          name: envoy-gateway-class
+      name: envoy-gateway/gateway-1
+      namespace: envoy-gateway-system
+xdsIR:
+  envoy-gateway/gateway-1:
+    accessLog:
+      json:
+      - path: /dev/stdout
+    globalResources:
+      proxyServiceCluster:
+        metadata:
+          name: envoy-envoy-gateway-gateway-1-196ae069
+          namespace: envoy-gateway-system
+          sectionName: "8080"
+        name: envoy-gateway/gateway-1
+        settings:
+        - addressType: IP
+          endpoints:
+          - host: 7.6.5.4
+            port: 8080
+            zone: zone1
+          metadata:
+            name: envoy-envoy-gateway-gateway-1-196ae069
+            namespace: envoy-gateway-system
+            sectionName: "8080"
+          name: envoy-gateway/gateway-1
+          protocol: TCP
+    http:
+    - address: 0.0.0.0
+      externalPort: 8081
+      hostnames:
+      - '*'
+      isHTTP2: false
+      metadata:
+        kind: Gateway
+        name: gateway-1
+        namespace: envoy-gateway
+        sectionName: listener-1
+      name: envoy-gateway/gateway-1/listener-1
+      path:
+        escapedSlashesAction: UnescapeAndRedirect
+        mergeSlashes: true
+      port: 8081
+      routes:
+      - destination:
+          metadata:
+            kind: HTTPRoute
+            name: httproute-1
+            namespace: default
+          name: httproute/default/httproute-1/rule/0
+          settings:
+          - addressType: IP
+            endpoints:
+            - host: 7.7.7.7
+              port: 8080
+            metadata:
+              name: service-1
+              namespace: default
+              sectionName: "8080"
+            name: httproute/default/httproute-1/rule/0/backend/0
+            protocol: HTTP
+            weight: 1
+        hostname: '*'
+        isHTTP2: false
+        metadata:
+          kind: HTTPRoute
+          name: httproute-1
+          namespace: default
+        name: httproute/default/httproute-1/rule/0/match/0/*
+        pathMatch:
+          distinct: false
+          name: ""
+          prefix: /foo
+    readyListener:
+      address: 0.0.0.0
+      ipFamily: IPv4
+      path: /ready
+      port: 19003
diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-section-name-override.in.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-section-name-override.in.yaml
new file mode 100644
index 000000000..24be74aec
--- /dev/null
+++ b/internal/gatewayapi/testdata/backendtrafficpolicy-section-name-override.in.yaml
@@ -0,0 +1,124 @@
+gateways:
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: Gateway
+  metadata:
+    namespace: envoy-gateway
+    name: gateway-1
+  spec:
+    gatewayClassName: envoy-gateway-class
+    listeners:
+    - name: listener-1
+      protocol: HTTP
+      port: 8081
+      allowedRoutes:
+        namespaces:
+          from: All
+    - name: listener-2
+      protocol: HTTP
+      port: 8082
+      allowedRoutes:
+        namespaces:
+          from: All
+grpcRoutes:
+- apiVersion: gateway.networking.k8s.io/v1alpha2
+  kind: GRPCRoute
+  metadata:
+    namespace: default
+    name: grpcroute-1
+  spec:
+    parentRefs:
+    - namespace: envoy-gateway
+      name: gateway-1
+      sectionName: listener-2
+    rules:
+    - backendRefs:
+      - name: service-1
+        port: 8080
+httpRoutes:
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: HTTPRoute
+  metadata:
+    namespace: default
+    name: httproute-1
+  spec:
+    parentRefs:
+    - namespace: envoy-gateway
+      name: gateway-1
+      sectionName: listener-1
+    rules:
+    - name: rule-1
+      matches:
+      - path:
+          value: "/foo"
+      backendRefs:
+      - name: service-1
+        port: 8080
+    - name: rule-2
+      matches:
+      - path:
+          value: "/bar"
+      backendRefs:
+      - name: service-1
+        port: 8080
+backendTrafficPolicies:
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+  kind: BackendTrafficPolicy
+  metadata:
+    namespace: envoy-gateway
+    name: policy-for-gateway-1
+  spec:
+    targetRef:
+      group: gateway.networking.k8s.io
+      kind: Gateway
+      name: gateway-1
+    timeout:
+      tcp:
+        connectTimeout: 15s
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+  kind: BackendTrafficPolicy
+  metadata:
+    namespace: envoy-gateway
+    name: policy-for-gateway-1-listener-2
+  spec:
+    targetRef:
+      group: gateway.networking.k8s.io
+      kind: Gateway
+      name: gateway-1
+      sectionName: listener-2
+    timeout:
+      tcp:
+        connectTimeout: 10s
+    httpUpgrade:
+    - type: websocket
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+  kind: BackendTrafficPolicy
+  metadata:
+    namespace: default
+    name: policy-for-route-1
+  spec:
+    targetRef:
+      group: gateway.networking.k8s.io
+      kind: HTTPRoute
+      name: httproute-1
+    timeout:
+      tcp:
+        connectTimeout: 15s
+      http:
+        connectionIdleTimeout: 16s
+        maxConnectionDuration: 17s
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+  kind: BackendTrafficPolicy
+  metadata:
+    namespace: default
+    name: policy-for-route-1-rule-1
+  spec:
+    targetRef:
+      group: gateway.networking.k8s.io
+      kind: HTTPRoute
+      name: httproute-1
+      sectionName: rule-1
+    timeout:
+      tcp:
+        connectTimeout: 15s
+      http:
+        connectionIdleTimeout: 20s
diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-section-name-override.out.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-section-name-override.out.yaml
new file mode 100644
index 000000000..b65a3299d
--- /dev/null
+++ b/internal/gatewayapi/testdata/backendtrafficpolicy-section-name-override.out.yaml
@@ -0,0 +1,474 @@
+backendTrafficPolicies:
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+  kind: BackendTrafficPolicy
+  metadata:
+    name: policy-for-route-1-rule-1
+    namespace: default
+  spec:
+    targetRef:
+      group: gateway.networking.k8s.io
+      kind: HTTPRoute
+      name: httproute-1
+      sectionName: rule-1
+    timeout:
+      http:
+        connectionIdleTimeout: 20s
+      tcp:
+        connectTimeout: 15s
+  status:
+    ancestors:
+    - ancestorRef:
+        group: gateway.networking.k8s.io
+        kind: Gateway
+        name: gateway-1
+        namespace: envoy-gateway
+        sectionName: listener-1
+      conditions:
+      - lastTransitionTime: null
+        message: Policy has been accepted.
+        reason: Accepted
+        status: "True"
+        type: Accepted
+      controllerName: gateway.envoyproxy.io/gatewayclass-controller
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+  kind: BackendTrafficPolicy
+  metadata:
+    name: policy-for-route-1
+    namespace: default
+  spec:
+    targetRef:
+      group: gateway.networking.k8s.io
+      kind: HTTPRoute
+      name: httproute-1
+    timeout:
+      http:
+        connectionIdleTimeout: 16s
+        maxConnectionDuration: 17s
+      tcp:
+        connectTimeout: 15s
+  status:
+    ancestors:
+    - ancestorRef:
+        group: gateway.networking.k8s.io
+        kind: Gateway
+        name: gateway-1
+        namespace: envoy-gateway
+        sectionName: listener-1
+      conditions:
+      - lastTransitionTime: null
+        message: Policy has been accepted.
+        reason: Accepted
+        status: "True"
+        type: Accepted
+      - lastTransitionTime: null
+        message: 'This policy is being overridden by other backendTrafficPolicy for
+          these route rules: [rule-1]'
+        reason: Overridden
+        status: "True"
+        type: Overridden
+      controllerName: gateway.envoyproxy.io/gatewayclass-controller
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+  kind: BackendTrafficPolicy
+  metadata:
+    name: policy-for-gateway-1-listener-2
+    namespace: envoy-gateway
+  spec:
+    httpUpgrade:
+    - type: websocket
+    targetRef:
+      group: gateway.networking.k8s.io
+      kind: Gateway
+      name: gateway-1
+      sectionName: listener-2
+    timeout:
+      tcp:
+        connectTimeout: 10s
+  status:
+    ancestors:
+    - ancestorRef:
+        group: gateway.networking.k8s.io
+        kind: Gateway
+        name: gateway-1
+        namespace: envoy-gateway
+        sectionName: listener-2
+      conditions:
+      - lastTransitionTime: null
+        message: Policy has been accepted.
+        reason: Accepted
+        status: "True"
+        type: Accepted
+      controllerName: gateway.envoyproxy.io/gatewayclass-controller
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+  kind: BackendTrafficPolicy
+  metadata:
+    name: policy-for-gateway-1
+    namespace: envoy-gateway
+  spec:
+    targetRef:
+      group: gateway.networking.k8s.io
+      kind: Gateway
+      name: gateway-1
+    timeout:
+      tcp:
+        connectTimeout: 15s
+  status:
+    ancestors:
+    - ancestorRef:
+        group: gateway.networking.k8s.io
+        kind: Gateway
+        name: gateway-1
+        namespace: envoy-gateway
+      conditions:
+      - lastTransitionTime: null
+        message: Policy has been accepted.
+        reason: Accepted
+        status: "True"
+        type: Accepted
+      - lastTransitionTime: null
+        message: 'This policy is being overridden by other backendTrafficPolicies
+          for these listeners: [listener-2] and these routes: [default/httproute-1]'
+        reason: Overridden
+        status: "True"
+        type: Overridden
+      controllerName: gateway.envoyproxy.io/gatewayclass-controller
+gateways:
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: Gateway
+  metadata:
+    name: gateway-1
+    namespace: envoy-gateway
+  spec:
+    gatewayClassName: envoy-gateway-class
+    listeners:
+    - allowedRoutes:
+        namespaces:
+          from: All
+      name: listener-1
+      port: 8081
+      protocol: HTTP
+    - allowedRoutes:
+        namespaces:
+          from: All
+      name: listener-2
+      port: 8082
+      protocol: HTTP
+  status:
+    listeners:
+    - attachedRoutes: 1
+      conditions:
+      - lastTransitionTime: null
+        message: Sending translated listener configuration to the data plane
+        reason: Programmed
+        status: "True"
+        type: Programmed
+      - lastTransitionTime: null
+        message: Listener has been successfully translated
+        reason: Accepted
+        status: "True"
+        type: Accepted
+      - lastTransitionTime: null
+        message: Listener references have been resolved
+        reason: ResolvedRefs
+        status: "True"
+        type: ResolvedRefs
+      name: listener-1
+      supportedKinds:
+      - group: gateway.networking.k8s.io
+        kind: HTTPRoute
+      - group: gateway.networking.k8s.io
+        kind: GRPCRoute
+    - attachedRoutes: 1
+      conditions:
+      - lastTransitionTime: null
+        message: Sending translated listener configuration to the data plane
+        reason: Programmed
+        status: "True"
+        type: Programmed
+      - lastTransitionTime: null
+        message: Listener has been successfully translated
+        reason: Accepted
+        status: "True"
+        type: Accepted
+      - lastTransitionTime: null
+        message: Listener references have been resolved
+        reason: ResolvedRefs
+        status: "True"
+        type: ResolvedRefs
+      name: listener-2
+      supportedKinds:
+      - group: gateway.networking.k8s.io
+        kind: HTTPRoute
+      - group: gateway.networking.k8s.io
+        kind: GRPCRoute
+grpcRoutes:
+- apiVersion: gateway.networking.k8s.io/v1alpha2
+  kind: GRPCRoute
+  metadata:
+    name: grpcroute-1
+    namespace: default
+  spec:
+    parentRefs:
+    - name: gateway-1
+      namespace: envoy-gateway
+      sectionName: listener-2
+    rules:
+    - backendRefs:
+      - name: service-1
+        port: 8080
+  status:
+    parents:
+    - conditions:
+      - lastTransitionTime: null
+        message: Route is accepted
+        reason: Accepted
+        status: "True"
+        type: Accepted
+      - lastTransitionTime: null
+        message: Resolved all the Object references for the Route
+        reason: ResolvedRefs
+        status: "True"
+        type: ResolvedRefs
+      controllerName: gateway.envoyproxy.io/gatewayclass-controller
+      parentRef:
+        name: gateway-1
+        namespace: envoy-gateway
+        sectionName: listener-2
+httpRoutes:
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: HTTPRoute
+  metadata:
+    name: httproute-1
+    namespace: default
+  spec:
+    parentRefs:
+    - name: gateway-1
+      namespace: envoy-gateway
+      sectionName: listener-1
+    rules:
+    - backendRefs:
+      - name: service-1
+        port: 8080
+      matches:
+      - path:
+          value: /foo
+      name: rule-1
+    - backendRefs:
+      - name: service-1
+        port: 8080
+      matches:
+      - path:
+          value: /bar
+      name: rule-2
+  status:
+    parents:
+    - conditions:
+      - lastTransitionTime: null
+        message: Route is accepted
+        reason: Accepted
+        status: "True"
+        type: Accepted
+      - lastTransitionTime: null
+        message: Resolved all the Object references for the Route
+        reason: ResolvedRefs
+        status: "True"
+        type: ResolvedRefs
+      controllerName: gateway.envoyproxy.io/gatewayclass-controller
+      parentRef:
+        name: gateway-1
+        namespace: envoy-gateway
+        sectionName: listener-1
+infraIR:
+  envoy-gateway/gateway-1:
+    proxy:
+      listeners:
+      - address: null
+        name: envoy-gateway/gateway-1/listener-1
+        ports:
+        - containerPort: 8081
+          name: http-8081
+          protocol: HTTP
+          servicePort: 8081
+      - address: null
+        name: envoy-gateway/gateway-1/listener-2
+        ports:
+        - containerPort: 8082
+          name: http-8082
+          protocol: HTTP
+          servicePort: 8082
+      metadata:
+        labels:
+          gateway.envoyproxy.io/owning-gateway-name: gateway-1
+          gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway
+        ownerReference:
+          kind: GatewayClass
+          name: envoy-gateway-class
+      name: envoy-gateway/gateway-1
+      namespace: envoy-gateway-system
+xdsIR:
+  envoy-gateway/gateway-1:
+    accessLog:
+      json:
+      - path: /dev/stdout
+    globalResources:
+      proxyServiceCluster:
+        metadata:
+          name: envoy-envoy-gateway-gateway-1-196ae069
+          namespace: envoy-gateway-system
+          sectionName: "8080"
+        name: envoy-gateway/gateway-1
+        settings:
+        - addressType: IP
+          endpoints:
+          - host: 7.6.5.4
+            port: 8080
+            zone: zone1
+          metadata:
+            name: envoy-envoy-gateway-gateway-1-196ae069
+            namespace: envoy-gateway-system
+            sectionName: "8080"
+          name: envoy-gateway/gateway-1
+          protocol: TCP
+    http:
+    - address: 0.0.0.0
+      externalPort: 8081
+      hostnames:
+      - '*'
+      isHTTP2: false
+      metadata:
+        kind: Gateway
+        name: gateway-1
+        namespace: envoy-gateway
+        sectionName: listener-1
+      name: envoy-gateway/gateway-1/listener-1
+      path:
+        escapedSlashesAction: UnescapeAndRedirect
+        mergeSlashes: true
+      port: 8081
+      routes:
+      - destination:
+          metadata:
+            kind: HTTPRoute
+            name: httproute-1
+            namespace: default
+            sectionName: rule-1
+          name: httproute/default/httproute-1/rule/0
+          settings:
+          - addressType: IP
+            endpoints:
+            - host: 7.7.7.7
+              port: 8080
+            metadata:
+              name: service-1
+              namespace: default
+              sectionName: "8080"
+            name: httproute/default/httproute-1/rule/0/backend/0
+            protocol: HTTP
+            weight: 1
+        hostname: '*'
+        isHTTP2: false
+        metadata:
+          kind: HTTPRoute
+          name: httproute-1
+          namespace: default
+          sectionName: rule-1
+        name: httproute/default/httproute-1/rule/0/match/0/*
+        pathMatch:
+          distinct: false
+          name: ""
+          prefix: /foo
+        traffic:
+          timeout:
+            http:
+              connectionIdleTimeout: 20s
+            tcp:
+              connectTimeout: 15s
+      - destination:
+          metadata:
+            kind: HTTPRoute
+            name: httproute-1
+            namespace: default
+            sectionName: rule-2
+          name: httproute/default/httproute-1/rule/1
+          settings:
+          - addressType: IP
+            endpoints:
+            - host: 7.7.7.7
+              port: 8080
+            metadata:
+              name: service-1
+              namespace: default
+              sectionName: "8080"
+            name: httproute/default/httproute-1/rule/1/backend/0
+            protocol: HTTP
+            weight: 1
+        hostname: '*'
+        isHTTP2: false
+        metadata:
+          kind: HTTPRoute
+          name: httproute-1
+          namespace: default
+          sectionName: rule-2
+        name: httproute/default/httproute-1/rule/1/match/0/*
+        pathMatch:
+          distinct: false
+          name: ""
+          prefix: /bar
+        traffic:
+          timeout:
+            http:
+              connectionIdleTimeout: 16s
+              maxConnectionDuration: 17s
+            tcp:
+              connectTimeout: 15s
+    - address: 0.0.0.0
+      externalPort: 8082
+      hostnames:
+      - '*'
+      isHTTP2: true
+      metadata:
+        kind: Gateway
+        name: gateway-1
+        namespace: envoy-gateway
+        sectionName: listener-2
+      name: envoy-gateway/gateway-1/listener-2
+      path:
+        escapedSlashesAction: UnescapeAndRedirect
+        mergeSlashes: true
+      port: 8082
+      routes:
+      - destination:
+          metadata:
+            kind: GRPCRoute
+            name: grpcroute-1
+            namespace: default
+          name: grpcroute/default/grpcroute-1/rule/0
+          settings:
+          - addressType: IP
+            endpoints:
+            - host: 7.7.7.7
+              port: 8080
+            metadata:
+              name: service-1
+              namespace: default
+              sectionName: "8080"
+            name: grpcroute/default/grpcroute-1/rule/0/backend/0
+            protocol: GRPC
+            weight: 1
+        hostname: '*'
+        isHTTP2: true
+        metadata:
+          kind: GRPCRoute
+          name: grpcroute-1
+          namespace: default
+        name: grpcroute/default/grpcroute-1/rule/0/match/-1/*
+        traffic:
+          httpUpgrade:
+          - type: websocket
+          timeout:
+            tcp:
+              connectTimeout: 10s
+    readyListener:
+      address: 0.0.0.0
+      ipFamily: IPv4
+      path: /ready
+      port: 19003
diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-strategic-merge-global-ratelimit.out.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-strategic-merge-global-ratelimit.out.yaml
index c9191d15b..d98800229 100644
--- a/internal/gatewayapi/testdata/backendtrafficpolicy-strategic-merge-global-ratelimit.out.yaml
+++ b/internal/gatewayapi/testdata/backendtrafficpolicy-strategic-merge-global-ratelimit.out.yaml
@@ -37,20 +37,13 @@ backendTrafficPolicies:
         kind: Gateway
         name: gateway-1
         namespace: envoy-gateway
+        sectionName: http
       conditions:
       - lastTransitionTime: null
         message: Merged with policy envoy-gateway/policy-for-gateway1
         reason: Merged
         status: "True"
         type: Merged
-      controllerName: gateway.envoyproxy.io/gatewayclass-controller
-    - ancestorRef:
-        group: gateway.networking.k8s.io
-        kind: Gateway
-        name: gateway-1
-        namespace: envoy-gateway
-        sectionName: http
-      conditions:
       - lastTransitionTime: null
         message: Policy has been accepted.
         reason: Accepted
diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-strategic-merge-local-ratelimit.out.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-strategic-merge-local-ratelimit.out.yaml
index a145ee2b1..9b7e433f6 100644
--- a/internal/gatewayapi/testdata/backendtrafficpolicy-strategic-merge-local-ratelimit.out.yaml
+++ b/internal/gatewayapi/testdata/backendtrafficpolicy-strategic-merge-local-ratelimit.out.yaml
@@ -36,20 +36,13 @@ backendTrafficPolicies:
         kind: Gateway
         name: gateway-1
         namespace: envoy-gateway
+        sectionName: http
       conditions:
       - lastTransitionTime: null
         message: Merged with policy envoy-gateway/policy-for-gateway1
         reason: Merged
         status: "True"
         type: Merged
-      controllerName: gateway.envoyproxy.io/gatewayclass-controller
-    - ancestorRef:
-        group: gateway.networking.k8s.io
-        kind: Gateway
-        name: gateway-1
-        namespace: envoy-gateway
-        sectionName: http
-      conditions:
       - lastTransitionTime: null
         message: Policy has been accepted.
         reason: Accepted
diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-strategic-merge-only-gw-rl.out.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-strategic-merge-only-gw-rl.out.yaml
index 816dea9ad..96d560de0 100644
--- a/internal/gatewayapi/testdata/backendtrafficpolicy-strategic-merge-only-gw-rl.out.yaml
+++ b/internal/gatewayapi/testdata/backendtrafficpolicy-strategic-merge-only-gw-rl.out.yaml
@@ -24,20 +24,13 @@ backendTrafficPolicies:
         kind: Gateway
         name: gateway-1
         namespace: envoy-gateway
+        sectionName: http
       conditions:
       - lastTransitionTime: null
         message: Merged with policy envoy-gateway/policy-for-gateway1
         reason: Merged
         status: "True"
         type: Merged
-      controllerName: gateway.envoyproxy.io/gatewayclass-controller
-    - ancestorRef:
-        group: gateway.networking.k8s.io
-        kind: Gateway
-        name: gateway-1
-        namespace: envoy-gateway
-        sectionName: http
-      conditions:
       - lastTransitionTime: null
         message: Policy has been accepted.
         reason: Accepted
diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-strategic-merge-only-httproute-rl.out.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-strategic-merge-only-httproute-rl.out.yaml
index 2077db86e..e2bbd4167 100644
--- a/internal/gatewayapi/testdata/backendtrafficpolicy-strategic-merge-only-httproute-rl.out.yaml
+++ b/internal/gatewayapi/testdata/backendtrafficpolicy-strategic-merge-only-httproute-rl.out.yaml
@@ -45,20 +45,13 @@ backendTrafficPolicies:
         kind: Gateway
         name: gateway-1
         namespace: envoy-gateway
+        sectionName: http
       conditions:
       - lastTransitionTime: null
         message: Merged with policy envoy-gateway/policy-for-gateway1
         reason: Merged
         status: "True"
         type: Merged
-      controllerName: gateway.envoyproxy.io/gatewayclass-controller
-    - ancestorRef:
-        group: gateway.networking.k8s.io
-        kind: Gateway
-        name: gateway-1
-        namespace: envoy-gateway
-        sectionName: http
-      conditions:
       - lastTransitionTime: null
         message: Policy has been accepted.
         reason: Accepted
diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-strategic-merge-section.in.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-strategic-merge-section.in.yaml
new file mode 100644
index 000000000..7bea49f06
--- /dev/null
+++ b/internal/gatewayapi/testdata/backendtrafficpolicy-strategic-merge-section.in.yaml
@@ -0,0 +1,140 @@
+gateways:
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: Gateway
+  metadata:
+    namespace: envoy-gateway
+    name: gateway-1
+  spec:
+    gatewayClassName: envoy-gateway-class
+    listeners:
+    - name: listener-1
+      protocol: HTTP
+      port: 8081
+      allowedRoutes:
+        namespaces:
+          from: All
+    - name: listener-2
+      protocol: HTTP
+      port: 8082
+      allowedRoutes:
+        namespaces:
+          from: All
+grpcRoutes:
+- apiVersion: gateway.networking.k8s.io/v1alpha2
+  kind: GRPCRoute
+  metadata:
+    namespace: default
+    name: grpcroute-1
+  spec:
+    parentRefs:
+    - namespace: envoy-gateway
+      name: gateway-1
+      sectionName: listener-2
+    rules:
+    - backendRefs:
+      - name: service-1
+        port: 8080
+httpRoutes:
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: HTTPRoute
+  metadata:
+    namespace: default
+    name: httproute-1
+  spec:
+    parentRefs:
+    - namespace: envoy-gateway
+      name: gateway-1
+      sectionName: listener-1
+    rules:
+    - name: rule-1
+      matches:
+      - path:
+          value: "/foo"
+      backendRefs:
+      - name: service-1
+        port: 8080
+    - name: rule-2
+      matches:
+      - path:
+          value: "/bar"
+      backendRefs:
+      - name: service-1
+        port: 8080
+backendTrafficPolicies:
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+  kind: BackendTrafficPolicy
+  metadata:
+    namespace: envoy-gateway
+    name: policy-for-gateway-1
+  spec:
+    targetRef:
+      group: gateway.networking.k8s.io
+      kind: Gateway
+      name: gateway-1
+    timeout:
+      tcp:
+        connectTimeout: 15s
+      http:
+        connectionIdleTimeout: 16s
+        maxConnectionDuration: 17s
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+  kind: BackendTrafficPolicy
+  metadata:
+    namespace: envoy-gateway
+    name: policy-for-gateway-1-listener-2
+  spec:
+    targetRef:
+      group: gateway.networking.k8s.io
+      kind: Gateway
+      name: gateway-1
+      sectionName: listener-2
+    timeout:
+      tcp:
+        connectTimeout: 25s
+      http:
+        connectionIdleTimeout: 26s
+        maxConnectionDuration: 27s
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+  kind: BackendTrafficPolicy
+  metadata:
+    namespace: default
+    name: merged-policy-for-httproute-1
+  spec:
+    targetRef:
+      group: gateway.networking.k8s.io
+      kind: HTTPRoute
+      name: httproute-1
+    mergeType: StrategicMerge
+    timeout:
+      tcp:
+        connectTimeout: 10s
+    connection:
+      bufferLimit: 100M
+    httpUpgrade:
+    - type: "spdy/3.1"
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+  kind: BackendTrafficPolicy
+  metadata:
+    namespace: default
+    name: policy-for-httproute-1-rule-2
+  spec:
+    targetRef:
+      group: gateway.networking.k8s.io
+      kind: HTTPRoute
+      name: httproute-1
+      sectionName: rule-2
+    connection:
+      bufferLimit: 200M
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+  kind: BackendTrafficPolicy
+  metadata:
+    namespace: default
+    name: merged-policy-for-grpcroute-1
+  spec:
+    targetRef:
+      group: gateway.networking.k8s.io
+      kind: GRPCRoute
+      name: grpcroute-1
+    mergeType: StrategicMerge
+    connection:
+      bufferLimit: 100M
diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-strategic-merge-section.out.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-strategic-merge-section.out.yaml
new file mode 100644
index 000000000..e36c39d22
--- /dev/null
+++ b/internal/gatewayapi/testdata/backendtrafficpolicy-strategic-merge-section.out.yaml
@@ -0,0 +1,531 @@
+backendTrafficPolicies:
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+  kind: BackendTrafficPolicy
+  metadata:
+    name: policy-for-httproute-1-rule-2
+    namespace: default
+  spec:
+    connection:
+      bufferLimit: 200M
+    targetRef:
+      group: gateway.networking.k8s.io
+      kind: HTTPRoute
+      name: httproute-1
+      sectionName: rule-2
+  status:
+    ancestors:
+    - ancestorRef:
+        group: gateway.networking.k8s.io
+        kind: Gateway
+        name: gateway-1
+        namespace: envoy-gateway
+        sectionName: listener-1
+      conditions:
+      - lastTransitionTime: null
+        message: Policy has been accepted.
+        reason: Accepted
+        status: "True"
+        type: Accepted
+      controllerName: gateway.envoyproxy.io/gatewayclass-controller
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+  kind: BackendTrafficPolicy
+  metadata:
+    name: merged-policy-for-httproute-1
+    namespace: default
+  spec:
+    connection:
+      bufferLimit: 100M
+    httpUpgrade:
+    - type: spdy/3.1
+    mergeType: StrategicMerge
+    targetRef:
+      group: gateway.networking.k8s.io
+      kind: HTTPRoute
+      name: httproute-1
+    timeout:
+      tcp:
+        connectTimeout: 10s
+  status:
+    ancestors:
+    - ancestorRef:
+        group: gateway.networking.k8s.io
+        kind: Gateway
+        name: gateway-1
+        namespace: envoy-gateway
+        sectionName: listener-1
+      conditions:
+      - lastTransitionTime: null
+        message: Merged with policy envoy-gateway/policy-for-gateway-1
+        reason: Merged
+        status: "True"
+        type: Merged
+      - lastTransitionTime: null
+        message: Policy has been accepted.
+        reason: Accepted
+        status: "True"
+        type: Accepted
+      - lastTransitionTime: null
+        message: 'This policy is being overridden by other backendTrafficPolicy for
+          these route rules: [rule-2]'
+        reason: Overridden
+        status: "True"
+        type: Overridden
+      controllerName: gateway.envoyproxy.io/gatewayclass-controller
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+  kind: BackendTrafficPolicy
+  metadata:
+    name: merged-policy-for-grpcroute-1
+    namespace: default
+  spec:
+    connection:
+      bufferLimit: 100M
+    mergeType: StrategicMerge
+    targetRef:
+      group: gateway.networking.k8s.io
+      kind: GRPCRoute
+      name: grpcroute-1
+  status:
+    ancestors:
+    - ancestorRef:
+        group: gateway.networking.k8s.io
+        kind: Gateway
+        name: gateway-1
+        namespace: envoy-gateway
+        sectionName: listener-2
+      conditions:
+      - lastTransitionTime: null
+        message: Merged with policy envoy-gateway/policy-for-gateway-1-listener-2
+        reason: Merged
+        status: "True"
+        type: Merged
+      - lastTransitionTime: null
+        message: Policy has been accepted.
+        reason: Accepted
+        status: "True"
+        type: Accepted
+      controllerName: gateway.envoyproxy.io/gatewayclass-controller
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+  kind: BackendTrafficPolicy
+  metadata:
+    name: policy-for-gateway-1-listener-2
+    namespace: envoy-gateway
+  spec:
+    targetRef:
+      group: gateway.networking.k8s.io
+      kind: Gateway
+      name: gateway-1
+      sectionName: listener-2
+    timeout:
+      http:
+        connectionIdleTimeout: 26s
+        maxConnectionDuration: 27s
+      tcp:
+        connectTimeout: 25s
+  status:
+    ancestors:
+    - ancestorRef:
+        group: gateway.networking.k8s.io
+        kind: Gateway
+        name: gateway-1
+        namespace: envoy-gateway
+        sectionName: listener-2
+      conditions:
+      - lastTransitionTime: null
+        message: Policy has been accepted.
+        reason: Accepted
+        status: "True"
+        type: Accepted
+      - lastTransitionTime: null
+        message: 'This policy is being merged by other backendTrafficPolicies for
+          these routes: [default/grpcroute-1]'
+        reason: Merged
+        status: "True"
+        type: Merged
+      controllerName: gateway.envoyproxy.io/gatewayclass-controller
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+  kind: BackendTrafficPolicy
+  metadata:
+    name: policy-for-gateway-1
+    namespace: envoy-gateway
+  spec:
+    targetRef:
+      group: gateway.networking.k8s.io
+      kind: Gateway
+      name: gateway-1
+    timeout:
+      http:
+        connectionIdleTimeout: 16s
+        maxConnectionDuration: 17s
+      tcp:
+        connectTimeout: 15s
+  status:
+    ancestors:
+    - ancestorRef:
+        group: gateway.networking.k8s.io
+        kind: Gateway
+        name: gateway-1
+        namespace: envoy-gateway
+      conditions:
+      - lastTransitionTime: null
+        message: Policy has been accepted.
+        reason: Accepted
+        status: "True"
+        type: Accepted
+      - lastTransitionTime: null
+        message: 'This policy is being merged by other backendTrafficPolicies for
+          these routes: [default/grpcroute-1 default/httproute-1]'
+        reason: Merged
+        status: "True"
+        type: Merged
+      - lastTransitionTime: null
+        message: 'This policy is being overridden by other backendTrafficPolicies
+          for these listeners: [listener-2]'
+        reason: Overridden
+        status: "True"
+        type: Overridden
+      controllerName: gateway.envoyproxy.io/gatewayclass-controller
+gateways:
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: Gateway
+  metadata:
+    name: gateway-1
+    namespace: envoy-gateway
+  spec:
+    gatewayClassName: envoy-gateway-class
+    listeners:
+    - allowedRoutes:
+        namespaces:
+          from: All
+      name: listener-1
+      port: 8081
+      protocol: HTTP
+    - allowedRoutes:
+        namespaces:
+          from: All
+      name: listener-2
+      port: 8082
+      protocol: HTTP
+  status:
+    listeners:
+    - attachedRoutes: 1
+      conditions:
+      - lastTransitionTime: null
+        message: Sending translated listener configuration to the data plane
+        reason: Programmed
+        status: "True"
+        type: Programmed
+      - lastTransitionTime: null
+        message: Listener has been successfully translated
+        reason: Accepted
+        status: "True"
+        type: Accepted
+      - lastTransitionTime: null
+        message: Listener references have been resolved
+        reason: ResolvedRefs
+        status: "True"
+        type: ResolvedRefs
+      name: listener-1
+      supportedKinds:
+      - group: gateway.networking.k8s.io
+        kind: HTTPRoute
+      - group: gateway.networking.k8s.io
+        kind: GRPCRoute
+    - attachedRoutes: 1
+      conditions:
+      - lastTransitionTime: null
+        message: Sending translated listener configuration to the data plane
+        reason: Programmed
+        status: "True"
+        type: Programmed
+      - lastTransitionTime: null
+        message: Listener has been successfully translated
+        reason: Accepted
+        status: "True"
+        type: Accepted
+      - lastTransitionTime: null
+        message: Listener references have been resolved
+        reason: ResolvedRefs
+        status: "True"
+        type: ResolvedRefs
+      name: listener-2
+      supportedKinds:
+      - group: gateway.networking.k8s.io
+        kind: HTTPRoute
+      - group: gateway.networking.k8s.io
+        kind: GRPCRoute
+grpcRoutes:
+- apiVersion: gateway.networking.k8s.io/v1alpha2
+  kind: GRPCRoute
+  metadata:
+    name: grpcroute-1
+    namespace: default
+  spec:
+    parentRefs:
+    - name: gateway-1
+      namespace: envoy-gateway
+      sectionName: listener-2
+    rules:
+    - backendRefs:
+      - name: service-1
+        port: 8080
+  status:
+    parents:
+    - conditions:
+      - lastTransitionTime: null
+        message: Route is accepted
+        reason: Accepted
+        status: "True"
+        type: Accepted
+      - lastTransitionTime: null
+        message: Resolved all the Object references for the Route
+        reason: ResolvedRefs
+        status: "True"
+        type: ResolvedRefs
+      controllerName: gateway.envoyproxy.io/gatewayclass-controller
+      parentRef:
+        name: gateway-1
+        namespace: envoy-gateway
+        sectionName: listener-2
+httpRoutes:
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: HTTPRoute
+  metadata:
+    name: httproute-1
+    namespace: default
+  spec:
+    parentRefs:
+    - name: gateway-1
+      namespace: envoy-gateway
+      sectionName: listener-1
+    rules:
+    - backendRefs:
+      - name: service-1
+        port: 8080
+      matches:
+      - path:
+          value: /foo
+      name: rule-1
+    - backendRefs:
+      - name: service-1
+        port: 8080
+      matches:
+      - path:
+          value: /bar
+      name: rule-2
+  status:
+    parents:
+    - conditions:
+      - lastTransitionTime: null
+        message: Route is accepted
+        reason: Accepted
+        status: "True"
+        type: Accepted
+      - lastTransitionTime: null
+        message: Resolved all the Object references for the Route
+        reason: ResolvedRefs
+        status: "True"
+        type: ResolvedRefs
+      controllerName: gateway.envoyproxy.io/gatewayclass-controller
+      parentRef:
+        name: gateway-1
+        namespace: envoy-gateway
+        sectionName: listener-1
+infraIR:
+  envoy-gateway/gateway-1:
+    proxy:
+      listeners:
+      - address: null
+        name: envoy-gateway/gateway-1/listener-1
+        ports:
+        - containerPort: 8081
+          name: http-8081
+          protocol: HTTP
+          servicePort: 8081
+      - address: null
+        name: envoy-gateway/gateway-1/listener-2
+        ports:
+        - containerPort: 8082
+          name: http-8082
+          protocol: HTTP
+          servicePort: 8082
+      metadata:
+        labels:
+          gateway.envoyproxy.io/owning-gateway-name: gateway-1
+          gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway
+        ownerReference:
+          kind: GatewayClass
+          name: envoy-gateway-class
+      name: envoy-gateway/gateway-1
+      namespace: envoy-gateway-system
+xdsIR:
+  envoy-gateway/gateway-1:
+    accessLog:
+      json:
+      - path: /dev/stdout
+    globalResources:
+      proxyServiceCluster:
+        metadata:
+          name: envoy-envoy-gateway-gateway-1-196ae069
+          namespace: envoy-gateway-system
+          sectionName: "8080"
+        name: envoy-gateway/gateway-1
+        settings:
+        - addressType: IP
+          endpoints:
+          - host: 7.6.5.4
+            port: 8080
+            zone: zone1
+          metadata:
+            name: envoy-envoy-gateway-gateway-1-196ae069
+            namespace: envoy-gateway-system
+            sectionName: "8080"
+          name: envoy-gateway/gateway-1
+          protocol: TCP
+    http:
+    - address: 0.0.0.0
+      externalPort: 8081
+      hostnames:
+      - '*'
+      isHTTP2: false
+      metadata:
+        kind: Gateway
+        name: gateway-1
+        namespace: envoy-gateway
+        sectionName: listener-1
+      name: envoy-gateway/gateway-1/listener-1
+      path:
+        escapedSlashesAction: UnescapeAndRedirect
+        mergeSlashes: true
+      port: 8081
+      routes:
+      - destination:
+          metadata:
+            kind: HTTPRoute
+            name: httproute-1
+            namespace: default
+            sectionName: rule-1
+          name: httproute/default/httproute-1/rule/0
+          settings:
+          - addressType: IP
+            endpoints:
+            - host: 7.7.7.7
+              port: 8080
+            metadata:
+              name: service-1
+              namespace: default
+              sectionName: "8080"
+            name: httproute/default/httproute-1/rule/0/backend/0
+            protocol: HTTP
+            weight: 1
+        hostname: '*'
+        isHTTP2: false
+        metadata:
+          kind: HTTPRoute
+          name: httproute-1
+          namespace: default
+          sectionName: rule-1
+        name: httproute/default/httproute-1/rule/0/match/0/*
+        pathMatch:
+          distinct: false
+          name: ""
+          prefix: /foo
+        traffic:
+          backendConnection:
+            bufferLimit: 100000000
+          httpUpgrade:
+          - type: spdy/3.1
+          timeout:
+            http:
+              connectionIdleTimeout: 16s
+              maxConnectionDuration: 17s
+            tcp:
+              connectTimeout: 10s
+      - destination:
+          metadata:
+            kind: HTTPRoute
+            name: httproute-1
+            namespace: default
+            sectionName: rule-2
+          name: httproute/default/httproute-1/rule/1
+          settings:
+          - addressType: IP
+            endpoints:
+            - host: 7.7.7.7
+              port: 8080
+            metadata:
+              name: service-1
+              namespace: default
+              sectionName: "8080"
+            name: httproute/default/httproute-1/rule/1/backend/0
+            protocol: HTTP
+            weight: 1
+        hostname: '*'
+        isHTTP2: false
+        metadata:
+          kind: HTTPRoute
+          name: httproute-1
+          namespace: default
+          sectionName: rule-2
+        name: httproute/default/httproute-1/rule/1/match/0/*
+        pathMatch:
+          distinct: false
+          name: ""
+          prefix: /bar
+        traffic:
+          backendConnection:
+            bufferLimit: 200000000
+    - address: 0.0.0.0
+      externalPort: 8082
+      hostnames:
+      - '*'
+      isHTTP2: true
+      metadata:
+        kind: Gateway
+        name: gateway-1
+        namespace: envoy-gateway
+        sectionName: listener-2
+      name: envoy-gateway/gateway-1/listener-2
+      path:
+        escapedSlashesAction: UnescapeAndRedirect
+        mergeSlashes: true
+      port: 8082
+      routes:
+      - destination:
+          metadata:
+            kind: GRPCRoute
+            name: grpcroute-1
+            namespace: default
+          name: grpcroute/default/grpcroute-1/rule/0
+          settings:
+          - addressType: IP
+            endpoints:
+            - host: 7.7.7.7
+              port: 8080
+            metadata:
+              name: service-1
+              namespace: default
+              sectionName: "8080"
+            name: grpcroute/default/grpcroute-1/rule/0/backend/0
+            protocol: GRPC
+            weight: 1
+        hostname: '*'
+        isHTTP2: true
+        metadata:
+          kind: GRPCRoute
+          name: grpcroute-1
+          namespace: default
+        name: grpcroute/default/grpcroute-1/rule/0/match/-1/*
+        traffic:
+          backendConnection:
+            bufferLimit: 100000000
+          timeout:
+            http:
+              connectionIdleTimeout: 26s
+              maxConnectionDuration: 27s
+            tcp:
+              connectTimeout: 25s
+    readyListener:
+      address: 0.0.0.0
+      ipFamily: IPv4
+      path: /ready
+      port: 19003
diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-strategic-merge-with-multi-parents.out.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-strategic-merge-with-multi-parents.out.yaml
index 7233ebea7..a68073a3e 100644
--- a/internal/gatewayapi/testdata/backendtrafficpolicy-strategic-merge-with-multi-parents.out.yaml
+++ b/internal/gatewayapi/testdata/backendtrafficpolicy-strategic-merge-with-multi-parents.out.yaml
@@ -24,20 +24,13 @@ backendTrafficPolicies:
         kind: Gateway
         name: gateway-1
         namespace: envoy-gateway
+        sectionName: http
       conditions:
       - lastTransitionTime: null
         message: Merged with policy envoy-gateway/policy-for-gateway1
         reason: Merged
         status: "True"
         type: Merged
-      controllerName: gateway.envoyproxy.io/gatewayclass-controller
-    - ancestorRef:
-        group: gateway.networking.k8s.io
-        kind: Gateway
-        name: gateway-1
-        namespace: envoy-gateway
-        sectionName: http
-      conditions:
       - lastTransitionTime: null
         message: Policy has been accepted.
         reason: Accepted
diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-strategic-merge.out.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-strategic-merge.out.yaml
index c5407dca1..3e4e8ada5 100644
--- a/internal/gatewayapi/testdata/backendtrafficpolicy-strategic-merge.out.yaml
+++ b/internal/gatewayapi/testdata/backendtrafficpolicy-strategic-merge.out.yaml
@@ -24,20 +24,13 @@ backendTrafficPolicies:
         kind: Gateway
         name: gateway-1
         namespace: envoy-gateway
+        sectionName: http
       conditions:
       - lastTransitionTime: null
         message: Merged with policy envoy-gateway/policy-for-gateway
         reason: Merged
         status: "True"
         type: Merged
-      controllerName: gateway.envoyproxy.io/gatewayclass-controller
-    - ancestorRef:
-        group: gateway.networking.k8s.io
-        kind: Gateway
-        name: gateway-1
-        namespace: envoy-gateway
-        sectionName: http
-      conditions:
       - lastTransitionTime: null
         message: Policy has been accepted.
         reason: Accepted
diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-with-ratelimit.in.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-with-ratelimit.in.yaml
index 678740706..74cae0fc6 100644
--- a/internal/gatewayapi/testdata/backendtrafficpolicy-with-ratelimit.in.yaml
+++ b/internal/gatewayapi/testdata/backendtrafficpolicy-with-ratelimit.in.yaml
@@ -27,6 +27,20 @@ gateways:
       allowedRoutes:
         namespaces:
           from: All
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: Gateway
+  metadata:
+    namespace: envoy-gateway
+    name: gateway-3
+  spec:
+    gatewayClassName: envoy-gateway-class
+    listeners:
+    - name: http
+      protocol: HTTP
+      port: 80
+      allowedRoutes:
+        namespaces:
+          from: All
 grpcRoutes:
 - apiVersion: gateway.networking.k8s.io/v1alpha2
   kind: GRPCRoute
@@ -42,6 +56,20 @@ grpcRoutes:
     - backendRefs:
       - name: service-1
         port: 8080
+- apiVersion: gateway.networking.k8s.io/v1alpha2
+  kind: GRPCRoute
+  metadata:
+    namespace: default
+    name: grpcroute-2
+  spec:
+    parentRefs:
+    - namespace: envoy-gateway
+      name: gateway-3
+      sectionName: http
+    rules:
+    - backendRefs:
+      - name: service-1
+        port: 8080
 httpRoutes:
 - apiVersion: gateway.networking.k8s.io/v1
   kind: HTTPRoute
@@ -119,3 +147,27 @@ backendTrafficPolicies:
               metadata:
                 namespace: something.com
                 key: some_cost_set_by_foo
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+  kind: BackendTrafficPolicy
+  metadata:
+    namespace: default
+    name: policy-for-grpc-route
+  spec:
+    targetRef:
+      group: gateway.networking.k8s.io
+      kind: GRPCRoute
+      name: grpcroute-2
+    rateLimit:
+      type: Global
+      global:
+        rules:
+        - clientSelectors:
+          - path:
+              type: Exact
+              value: /test
+          - methods:
+            - value: GET
+              invert: true
+          limit:
+            requests: 30
+            unit: Hour
diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-with-ratelimit.out.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-with-ratelimit.out.yaml
index d69afacfc..ca29c7923 100644
--- a/internal/gatewayapi/testdata/backendtrafficpolicy-with-ratelimit.out.yaml
+++ b/internal/gatewayapi/testdata/backendtrafficpolicy-with-ratelimit.out.yaml
@@ -44,6 +44,45 @@ backendTrafficPolicies:
         status: "True"
         type: Accepted
       controllerName: gateway.envoyproxy.io/gatewayclass-controller
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+  kind: BackendTrafficPolicy
+  metadata:
+    name: policy-for-grpc-route
+    namespace: default
+  spec:
+    rateLimit:
+      global:
+        rules:
+        - clientSelectors:
+          - path:
+              type: Exact
+              value: /test
+          - methods:
+            - invert: true
+              value: GET
+          limit:
+            requests: 30
+            unit: Hour
+      type: Global
+    targetRef:
+      group: gateway.networking.k8s.io
+      kind: GRPCRoute
+      name: grpcroute-2
+  status:
+    ancestors:
+    - ancestorRef:
+        group: gateway.networking.k8s.io
+        kind: Gateway
+        name: gateway-3
+        namespace: envoy-gateway
+        sectionName: http
+      conditions:
+      - lastTransitionTime: null
+        message: Policy has been accepted.
+        reason: Accepted
+        status: "True"
+        type: Accepted
+      controllerName: gateway.envoyproxy.io/gatewayclass-controller
 - apiVersion: gateway.envoyproxy.io/v1alpha1
   kind: BackendTrafficPolicy
   metadata:
@@ -163,6 +202,45 @@ gateways:
         kind: HTTPRoute
       - group: gateway.networking.k8s.io
         kind: GRPCRoute
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: Gateway
+  metadata:
+    name: gateway-3
+    namespace: envoy-gateway
+  spec:
+    gatewayClassName: envoy-gateway-class
+    listeners:
+    - allowedRoutes:
+        namespaces:
+          from: All
+      name: http
+      port: 80
+      protocol: HTTP
+  status:
+    listeners:
+    - attachedRoutes: 1
+      conditions:
+      - lastTransitionTime: null
+        message: Sending translated listener configuration to the data plane
+        reason: Programmed
+        status: "True"
+        type: Programmed
+      - lastTransitionTime: null
+        message: Listener has been successfully translated
+        reason: Accepted
+        status: "True"
+        type: Accepted
+      - lastTransitionTime: null
+        message: Listener references have been resolved
+        reason: ResolvedRefs
+        status: "True"
+        type: ResolvedRefs
+      name: http
+      supportedKinds:
+      - group: gateway.networking.k8s.io
+        kind: HTTPRoute
+      - group: gateway.networking.k8s.io
+        kind: GRPCRoute
 grpcRoutes:
 - apiVersion: gateway.networking.k8s.io/v1alpha2
   kind: GRPCRoute
@@ -196,6 +274,38 @@ grpcRoutes:
         name: gateway-1
         namespace: envoy-gateway
         sectionName: http
+- apiVersion: gateway.networking.k8s.io/v1alpha2
+  kind: GRPCRoute
+  metadata:
+    name: grpcroute-2
+    namespace: default
+  spec:
+    parentRefs:
+    - name: gateway-3
+      namespace: envoy-gateway
+      sectionName: http
+    rules:
+    - backendRefs:
+      - name: service-1
+        port: 8080
+  status:
+    parents:
+    - conditions:
+      - lastTransitionTime: null
+        message: Route is accepted
+        reason: Accepted
+        status: "True"
+        type: Accepted
+      - lastTransitionTime: null
+        message: Resolved all the Object references for the Route
+        reason: ResolvedRefs
+        status: "True"
+        type: ResolvedRefs
+      controllerName: gateway.envoyproxy.io/gatewayclass-controller
+      parentRef:
+        name: gateway-3
+        namespace: envoy-gateway
+        sectionName: http
 httpRoutes:
 - apiVersion: gateway.networking.k8s.io/v1
   kind: HTTPRoute
@@ -273,6 +383,25 @@ infraIR:
           name: envoy-gateway-class
       name: envoy-gateway/gateway-2
       namespace: envoy-gateway-system
+  envoy-gateway/gateway-3:
+    proxy:
+      listeners:
+      - address: null
+        name: envoy-gateway/gateway-3/http
+        ports:
+        - containerPort: 10080
+          name: http-80
+          protocol: HTTP
+          servicePort: 80
+      metadata:
+        labels:
+          gateway.envoyproxy.io/owning-gateway-name: gateway-3
+          gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway
+        ownerReference:
+          kind: GatewayClass
+          name: envoy-gateway-class
+      name: envoy-gateway/gateway-3
+      namespace: envoy-gateway-system
 xdsIR:
   envoy-gateway/gateway-1:
     accessLog:
@@ -463,3 +592,95 @@ xdsIR:
       ipFamily: IPv4
       path: /ready
       port: 19003
+  envoy-gateway/gateway-3:
+    accessLog:
+      json:
+      - path: /dev/stdout
+    globalResources:
+      envoyClientCertificate:
+        certificate: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUREVENDQWZXZ0F3SUJBZ0lVRUZNaFA5ZUo5WEFCV3NRNVptNmJSazJjTE5Rd0RRWUpLb1pJaHZjTkFRRUwKQlFBd0ZqRVVNQklHQTFVRUF3d0xabTl2TG1KaGNpNWpiMjB3SGhjTk1qUXdNakk1TURrek1ERXdXaGNOTXpRdwpNakkyTURrek1ERXdXakFXTVJRd0VnWURWUVFEREF0bWIyOHVZbUZ5TG1OdmJUQ0NBU0l3RFFZSktvWklodmNOCkFRRUJCUUFEZ2dFUEFEQ0NBUW9DZ2dFQkFKbEk2WXhFOVprQ1BzNnBDUXhickNtZWl4OVA1RGZ4OVJ1NUxENFQKSm1kVzdJS2R0UVYvd2ZMbXRzdTc2QithVGRDaldlMEJUZmVPT1JCYlIzY1BBRzZFbFFMaWNsUVVydW4zcStncwpKcEsrSTdjSStqNXc4STY4WEg1V1E3clZVdGJ3SHBxYncrY1ZuQnFJVU9MaUlhdGpJZjdLWDUxTTF1RjljZkVICkU0RG5jSDZyYnI1OS9SRlpCc2toeHM1T3p3Sklmb2hreXZGd2V1VHd4Sy9WcGpJKzdPYzQ4QUJDWHBOTzlEL3EKRWgrck9hdWpBTWNYZ0hRSVRrQ2lpVVRjVW82TFNIOXZMWlB0YXFmem9acTZuaE1xcFc2NUUxcEF3RjNqeVRUeAphNUk4SmNmU0Zqa2llWjIwTFVRTW43TThVNHhIamFvL2d2SDBDQWZkQjdSTFUyc0NBd0VBQWFOVE1GRXdIUVlEClZSME9CQllFRk9SQ0U4dS8xRERXN2loWnA3Y3g5dFNtUG02T01COEdBMVVkSXdRWU1CYUFGT1JDRTh1LzFERFcKN2loWnA3Y3g5dFNtUG02T01BOEdBMVVkRXdFQi93UUZNQU1CQWY4d0RRWUpLb1pJaHZjTkFRRUxCUUFEZ2dFQgpBRnQ1M3pqc3FUYUg1YThFMmNodm1XQWdDcnhSSzhiVkxNeGl3TkdqYm1FUFJ6K3c2TngrazBBOEtFY0lEc0tjClNYY2k1OHU0b1didFZKQmx6YS9adWpIUjZQMUJuT3BsK2FveTc4NGJiZDRQMzl3VExvWGZNZmJCQ20xdmV2aDkKQUpLbncyWnRxcjRta2JMY3hFcWxxM3NCTEZBUzlzUUxuS05DZTJjR0xkVHAyYm9HK3FjZ3lRZ0NJTTZmOEVNdgpXUGlmQ01NR3V6Sy9HUkY0YlBPL1lGNDhld0R1M1VlaWgwWFhkVUFPRTlDdFVhOE5JaGMxVVBhT3pQcnRZVnFyClpPR2t2L0t1K0I3OGg4U0VzTzlYclFjdXdiT25KeDZLdFIrYWV5a3ZBcFhDUTNmWkMvYllLQUFSK1A4QUpvUVoKYndJVW1YaTRnajVtK2JLUGhlK2lyK0U9Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0=
+        name: envoy-gateway-system/envoy
+        privateKey: '[redacted]'
+      proxyServiceCluster:
+        metadata:
+          name: envoy-envoy-gateway-gateway-3-32e857a6
+          namespace: envoy-gateway-system
+          sectionName: "8080"
+        name: envoy-gateway/gateway-3
+        settings:
+        - addressType: IP
+          endpoints:
+          - host: 7.6.5.4
+            port: 8080
+            zone: zone1
+          metadata:
+            name: envoy-envoy-gateway-gateway-3-32e857a6
+            namespace: envoy-gateway-system
+            sectionName: "8080"
+          name: envoy-gateway/gateway-3
+          protocol: TCP
+    http:
+    - address: 0.0.0.0
+      externalPort: 80
+      hostnames:
+      - '*'
+      isHTTP2: true
+      metadata:
+        kind: Gateway
+        name: gateway-3
+        namespace: envoy-gateway
+        sectionName: http
+      name: envoy-gateway/gateway-3/http
+      path:
+        escapedSlashesAction: UnescapeAndRedirect
+        mergeSlashes: true
+      port: 10080
+      routes:
+      - destination:
+          metadata:
+            kind: GRPCRoute
+            name: grpcroute-2
+            namespace: default
+          name: grpcroute/default/grpcroute-2/rule/0
+          settings:
+          - addressType: IP
+            endpoints:
+            - host: 7.7.7.7
+              port: 8080
+            metadata:
+              name: service-1
+              namespace: default
+              sectionName: "8080"
+            name: grpcroute/default/grpcroute-2/rule/0/backend/0
+            protocol: GRPC
+            weight: 1
+        hostname: '*'
+        isHTTP2: true
+        metadata:
+          kind: GRPCRoute
+          name: grpcroute-2
+          namespace: default
+        name: grpcroute/default/grpcroute-2/rule/0/match/-1/*
+        traffic:
+          rateLimit:
+            global:
+              rules:
+              - headerMatches: []
+                limit:
+                  requests: 30
+                  unit: Hour
+                methodMatches:
+                - distinct: false
+                  exact: GET
+                  invert: true
+                  name: ""
+                name: default/policy-for-grpc-route/rule/0
+                pathMatch:
+                  distinct: false
+                  exact: /test
+                  name: ""
+    readyListener:
+      address: 0.0.0.0
+      ipFamily: IPv4
+      path: /ready
+      port: 19003
diff --git a/internal/gatewayapi/testdata/clienttrafficpolicy-headers-error.out.yaml b/internal/gatewayapi/testdata/clienttrafficpolicy-headers-error.out.yaml
index 33061b877..d46377c17 100644
--- a/internal/gatewayapi/testdata/clienttrafficpolicy-headers-error.out.yaml
+++ b/internal/gatewayapi/testdata/clienttrafficpolicy-headers-error.out.yaml
@@ -168,7 +168,7 @@ xdsIR:
       externalPort: 80
       headers:
         enableEnvoyHeaders: true
-        requestID: Preserve
+        requestID: PreserveOrGenerate
         withUnderscoresAction: Allow
       hostnames:
       - '*'
diff --git a/internal/gatewayapi/testdata/httproute-with-cors-filter.in.yaml b/internal/gatewayapi/testdata/httproute-with-cors-filter.in.yaml
index e143c40f4..6659df8f8 100644
--- a/internal/gatewayapi/testdata/httproute-with-cors-filter.in.yaml
+++ b/internal/gatewayapi/testdata/httproute-with-cors-filter.in.yaml
@@ -52,7 +52,6 @@ httpRoutes:
           - header4
           allowCredentials: true
           maxAge: 1000
-
 - apiVersion: gateway.networking.k8s.io/v1
   kind: HTTPRoute
   metadata:
@@ -82,3 +81,35 @@ httpRoutes:
           - POST
           - PUT
           - DELETE
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: HTTPRoute
+  metadata:
+    namespace: default
+    name: httproute-cors-specific-methods
+  spec:
+    hostnames:
+    - gateway.envoyproxy.io
+    parentRefs:
+    - namespace: envoy-gateway
+      name: gateway-1
+      sectionName: http
+    rules:
+    - matches:
+      - path:
+          value: "/baz"
+        method: "GET"
+        queryParams:
+        - type: RegularExpression
+          name: id
+          value: '.*'
+      backendRefs:
+      - name: service-1
+        port: 8080
+      filters:
+      - type: CORS
+        cors:
+          allowOrigins:
+          - "https://gateway.envoyproxy.io"
+          allowMethods:
+          - OPTIONS
+          - GET
diff --git a/internal/gatewayapi/testdata/httproute-with-cors-filter.out.yaml b/internal/gatewayapi/testdata/httproute-with-cors-filter.out.yaml
index 87c6841ae..bda8e47b0 100644
--- a/internal/gatewayapi/testdata/httproute-with-cors-filter.out.yaml
+++ b/internal/gatewayapi/testdata/httproute-with-cors-filter.out.yaml
@@ -16,7 +16,7 @@ gateways:
       protocol: HTTP
   status:
     listeners:
-    - attachedRoutes: 2
+    - attachedRoutes: 3
       conditions:
       - lastTransitionTime: null
         message: Sending translated listener configuration to the data plane
@@ -142,6 +142,56 @@ httpRoutes:
         name: gateway-1
         namespace: envoy-gateway
         sectionName: http
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: HTTPRoute
+  metadata:
+    name: httproute-cors-specific-methods
+    namespace: default
+  spec:
+    hostnames:
+    - gateway.envoyproxy.io
+    parentRefs:
+    - name: gateway-1
+      namespace: envoy-gateway
+      sectionName: http
+    rules:
+    - backendRefs:
+      - name: service-1
+        port: 8080
+      filters:
+      - cors:
+          allowMethods:
+          - OPTIONS
+          - GET
+          allowOrigins:
+          - https://gateway.envoyproxy.io
+        type: CORS
+      matches:
+      - method: GET
+        path:
+          value: /baz
+        queryParams:
+        - name: id
+          type: RegularExpression
+          value: .*
+  status:
+    parents:
+    - conditions:
+      - lastTransitionTime: null
+        message: Route is accepted
+        reason: Accepted
+        status: "True"
+        type: Accepted
+      - lastTransitionTime: null
+        message: Resolved all the Object references for the Route
+        reason: ResolvedRefs
+        status: "True"
+        type: ResolvedRefs
+      controllerName: gateway.envoyproxy.io/gatewayclass-controller
+      parentRef:
+        name: gateway-1
+        namespace: envoy-gateway
+        sectionName: http
 infraIR:
   envoy-gateway/gateway-1:
     proxy:
@@ -203,6 +253,104 @@ xdsIR:
         mergeSlashes: true
       port: 10080
       routes:
+      - cors:
+          allowMethods:
+          - OPTIONS
+          - GET
+          allowOrigins:
+          - distinct: false
+            exact: https://gateway.envoyproxy.io
+            name: ""
+          maxAge: 0s
+        destination:
+          metadata:
+            kind: HTTPRoute
+            name: httproute-cors-specific-methods
+            namespace: default
+          name: httproute/default/httproute-cors-specific-methods/rule/0
+          settings:
+          - addressType: IP
+            endpoints:
+            - host: 7.7.7.7
+              port: 8080
+            metadata:
+              name: service-1
+              namespace: default
+              sectionName: "8080"
+            name: httproute/default/httproute-cors-specific-methods/rule/0/backend/0
+            protocol: HTTP
+            weight: 1
+        headerMatches:
+        - distinct: false
+          exact: OPTIONS
+          name: :method
+        - distinct: false
+          name: origin
+          safeRegex: .*
+        - distinct: false
+          name: access-control-request-method
+          safeRegex: .*
+        hostname: gateway.envoyproxy.io
+        isHTTP2: false
+        metadata:
+          kind: HTTPRoute
+          name: httproute-cors-specific-methods
+          namespace: default
+        name: httproute/default/httproute-cors-specific-methods/rule/0/match/0/cors-preflight/gateway_envoyproxy_io
+        pathMatch:
+          distinct: false
+          name: ""
+          prefix: /baz
+        queryParamMatches:
+        - distinct: false
+          name: id
+          safeRegex: .*
+      - cors:
+          allowMethods:
+          - OPTIONS
+          - GET
+          allowOrigins:
+          - distinct: false
+            exact: https://gateway.envoyproxy.io
+            name: ""
+          maxAge: 0s
+        destination:
+          metadata:
+            kind: HTTPRoute
+            name: httproute-cors-specific-methods
+            namespace: default
+          name: httproute/default/httproute-cors-specific-methods/rule/0
+          settings:
+          - addressType: IP
+            endpoints:
+            - host: 7.7.7.7
+              port: 8080
+            metadata:
+              name: service-1
+              namespace: default
+              sectionName: "8080"
+            name: httproute/default/httproute-cors-specific-methods/rule/0/backend/0
+            protocol: HTTP
+            weight: 1
+        headerMatches:
+        - distinct: false
+          exact: GET
+          name: :method
+        hostname: gateway.envoyproxy.io
+        isHTTP2: false
+        metadata:
+          kind: HTTPRoute
+          name: httproute-cors-specific-methods
+          namespace: default
+        name: httproute/default/httproute-cors-specific-methods/rule/0/match/0/gateway_envoyproxy_io
+        pathMatch:
+          distinct: false
+          name: ""
+          prefix: /baz
+        queryParamMatches:
+        - distinct: false
+          name: id
+          safeRegex: .*
       - cors:
           allowCredentials: true
           allowHeaders:
diff --git a/internal/infrastructure/kubernetes/ratelimit/resource_provider_test.go b/internal/infrastructure/kubernetes/ratelimit/resource_provider_test.go
index b281a346b..a881b2c4e 100644
--- a/internal/infrastructure/kubernetes/ratelimit/resource_provider_test.go
+++ b/internal/infrastructure/kubernetes/ratelimit/resource_provider_test.go
@@ -11,15 +11,16 @@ import (
 	"strconv"
 	"testing"
 
-	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 	appsv1 "k8s.io/api/apps/v1"
 	autoscalingv2 "k8s.io/api/autoscaling/v2"
 	corev1 "k8s.io/api/core/v1"
+	policyv1 "k8s.io/api/policy/v1"
 	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
 	"k8s.io/apimachinery/pkg/api/resource"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/types"
+	"k8s.io/apimachinery/pkg/util/intstr"
 	"k8s.io/utils/ptr"
 	gwapiv1 "sigs.k8s.io/gateway-api/apis/v1"
 	"sigs.k8s.io/yaml"
@@ -103,20 +104,7 @@ func TestServiceAccount(t *testing.T) {
 	sa, err := r.ServiceAccount()
 	require.NoError(t, err)
 
-	expected, err := loadServiceAccount()
-	require.NoError(t, err)
-
-	assert.Equal(t, expected, sa)
-}
-
-func loadServiceAccount() (*corev1.ServiceAccount, error) {
-	saYAML, err := os.ReadFile("testdata/envoy-ratelimit-serviceaccount.yaml")
-	if err != nil {
-		return nil, err
-	}
-	sa := &corev1.ServiceAccount{}
-	_ = yaml.Unmarshal(saYAML, sa)
-	return sa, nil
+	requireObject[corev1.ServiceAccount](t, sa, "testdata/envoy-ratelimit-serviceaccount.yaml")
 }
 
 func TestService(t *testing.T) {
@@ -164,22 +152,8 @@ func TestService(t *testing.T) {
 
 		svc, err := r.Service()
 		require.NoError(t, err)
-
-		expected, err := loadService(tc.caseName)
-		require.NoError(t, err)
-
-		assert.Equal(t, expected, svc)
-	}
-}
-
-func loadService(caseName string) (*corev1.Service, error) {
-	serviceYAML, err := os.ReadFile(fmt.Sprintf("testdata/services/envoy-ratelimit-service-%s.yaml", caseName))
-	if err != nil {
-		return nil, err
+		requireObject[corev1.Service](t, svc, fmt.Sprintf("testdata/services/envoy-ratelimit-service-%s.yaml", tc.caseName))
 	}
-	svc := &corev1.Service{}
-	_ = yaml.Unmarshal(serviceYAML, svc)
-	return svc, nil
 }
 
 func TestConfigmap(t *testing.T) {
@@ -197,30 +171,81 @@ func TestConfigmap(t *testing.T) {
 	r := NewResourceRender(cfg.ControllerNamespace, cfg.EnvoyGateway, ownerReferenceUID)
 	cm, err := r.ConfigMap("")
 	require.NoError(t, err)
+	requireObject[corev1.ConfigMap](t, cm, "testdata/envoy-ratelimit-configmap.yaml")
+}
 
-	if test.OverrideTestData() {
-		cmYAML, err := yaml.Marshal(cm)
-		require.NoError(t, err)
-		// nolint:gosec
-		err = os.WriteFile("testdata/envoy-ratelimit-configmap.yaml", cmYAML, 0o644)
-		require.NoError(t, err)
-		return
-	}
-
-	expected, err := loadConfigmap()
+func TestPDB(t *testing.T) {
+	cfg, err := config.New(os.Stdout, os.Stderr)
 	require.NoError(t, err)
+	cases := []struct {
+		caseName  string
+		rateLimit *egv1a1.RateLimit
+		pdb       *egv1a1.KubernetesPodDisruptionBudgetSpec
+	}{
+		{
+			caseName: "default",
+			rateLimit: &egv1a1.RateLimit{
+				Backend: egv1a1.RateLimitDatabaseBackend{
+					Type: egv1a1.RedisBackendType,
+					Redis: &egv1a1.RateLimitRedisSettings{
+						URL: "redis.redis.svc:6379",
+					},
+				},
+			},
+			pdb: &egv1a1.KubernetesPodDisruptionBudgetSpec{},
+		},
+		{
+			caseName: "custom",
+			rateLimit: &egv1a1.RateLimit{
+				Backend: egv1a1.RateLimitDatabaseBackend{
+					Type: egv1a1.RedisBackendType,
+					Redis: &egv1a1.RateLimitRedisSettings{
+						URL: "redis.redis.svc:6379",
+					},
+				},
+			},
+			pdb: &egv1a1.KubernetesPodDisruptionBudgetSpec{
+				MinAvailable: &intstr.IntOrString{Type: intstr.Int, IntVal: 10},
+				Patch: &egv1a1.KubernetesPatchSpec{
+					Type: ptr.To(egv1a1.StrategicMerge),
+					Value: apiextensionsv1.JSON{
+						Raw: []byte("{\"spec\":{\"maxUnavailable\": 11}}"),
+					},
+				},
+			},
+		},
+		{
+			caseName: "with-name",
+			rateLimit: &egv1a1.RateLimit{
+				Backend: egv1a1.RateLimitDatabaseBackend{
+					Type: egv1a1.RedisBackendType,
+					Redis: &egv1a1.RateLimitRedisSettings{
+						URL: "redis.redis.svc:6379",
+					},
+				},
+			},
+			pdb: &egv1a1.KubernetesPodDisruptionBudgetSpec{
+				Name: ptr.To("custom-pdb-name"),
+			},
+		},
+	}
+	for _, tc := range cases {
+		t.Run(tc.caseName, func(t *testing.T) {
+			cfg.EnvoyGateway.RateLimit = tc.rateLimit
 
-	assert.Equal(t, expected, cm)
-}
+			cfg.EnvoyGateway.Provider = &egv1a1.EnvoyGatewayProvider{
+				Type: egv1a1.ProviderTypeKubernetes,
+				Kubernetes: &egv1a1.EnvoyGatewayKubernetesProvider{
+					RateLimitPDB: tc.pdb,
+				},
+			}
+			r := NewResourceRender(cfg.ControllerNamespace, cfg.EnvoyGateway, ownerReferenceUID)
+			pdb, err := r.PodDisruptionBudget()
+			require.NoError(t, err)
 
-func loadConfigmap() (*corev1.ConfigMap, error) {
-	configmapYAML, err := os.ReadFile("testdata/envoy-ratelimit-configmap.yaml")
-	if err != nil {
-		return nil, err
+			requireObject[policyv1.PodDisruptionBudget](t, pdb, fmt.Sprintf("testdata/pdb/%s.yaml", tc.caseName))
+		})
 	}
-	cm := &corev1.ConfigMap{}
-	_ = yaml.Unmarshal(configmapYAML, cm)
-	return cm, nil
 }
 
 func TestDeployment(t *testing.T) {
@@ -767,34 +792,11 @@ func TestDeployment(t *testing.T) {
 			r := NewResourceRender(cfg.ControllerNamespace, cfg.EnvoyGateway, ownerReferenceUID)
 			dp, err := r.Deployment()
 			require.NoError(t, err)
-
-			if test.OverrideTestData() {
-				deploymentYAML, err := yaml.Marshal(dp)
-				require.NoError(t, err)
-				// nolint:gosec
-				err = os.WriteFile(fmt.Sprintf("testdata/deployments/%s.yaml", tc.caseName), deploymentYAML, 0o644)
-				require.NoError(t, err)
-				return
-			}
-
-			expected, err := loadDeployment(tc.caseName)
-			require.NoError(t, err)
-
-			assert.Equal(t, expected, dp)
+			requireObject[appsv1.Deployment](t, dp, fmt.Sprintf("testdata/deployments/%s.yaml", tc.caseName))
 		})
 	}
 }
 
-func loadDeployment(caseName string) (*appsv1.Deployment, error) {
-	deploymentYAML, err := os.ReadFile(fmt.Sprintf("testdata/deployments/%s.yaml", caseName))
-	if err != nil {
-		return nil, err
-	}
-	deployment := &appsv1.Deployment{}
-	_ = yaml.Unmarshal(deploymentYAML, deployment)
-	return deployment, nil
-}
-
 func TestHorizontalPodAutoscaler(t *testing.T) {
 	cfg, err := config.New(os.Stdout, os.Stderr)
 	require.NoError(t, err)
@@ -898,35 +900,38 @@ func TestHorizontalPodAutoscaler(t *testing.T) {
 			hpa, err := r.HorizontalPodAutoscaler()
 			require.NoError(t, err)
 
-			if test.OverrideTestData() {
-				hpaYAML, err := yaml.Marshal(hpa)
-				require.NoError(t, err)
-				// nolint:gosec
-				err = os.WriteFile(fmt.Sprintf("testdata/hpa/%s.yaml", tc.caseName), hpaYAML, 0o644)
-				require.NoError(t, err)
-				return
-			}
-
-			expected, err := loadHpa(tc.caseName)
-			require.NoError(t, err)
-
-			assert.Equal(t, expected, hpa)
+			requireObject[autoscalingv2.HorizontalPodAutoscaler](t, hpa, fmt.Sprintf("testdata/hpa/%s.yaml", tc.caseName))
 		})
 	}
 }
 
-func loadHpa(caseName string) (*autoscalingv2.HorizontalPodAutoscaler, error) {
-	hpaYAML, err := os.ReadFile(fmt.Sprintf("testdata/hpa/%s.yaml", caseName))
+func loadObject[T any](filename string) (*T, error) {
+	obj := new(T)
+	data, err := os.ReadFile(filename)
 	if err != nil {
-		return nil, err
+		return obj, err
+	}
+
+	_ = yaml.Unmarshal(data, obj)
+	return obj, nil
+}
+
+func requireObject[ObjectT any](t *testing.T, got *ObjectT, filename string) {
+	if test.OverrideTestData() {
+		cmYAML, err := yaml.Marshal(got)
+		require.NoError(t, err)
+		// nolint:gosec
+		err = os.WriteFile(filename, cmYAML, 0o644)
+		require.NoError(t, err)
+		return
 	}
 
-	hpa := &autoscalingv2.HorizontalPodAutoscaler{}
-	_ = yaml.Unmarshal(hpaYAML, hpa)
-	return hpa, nil
+	expected, err := loadObject[ObjectT](filename)
+	require.NoError(t, err)
+	require.Equal(t, expected, got)
 }
 
 func TestGetServiceURL(t *testing.T) {
 	got := GetServiceURL("envoy-gateway-system", "example-cluster.local")
-	assert.Equal(t, "grpc://envoy-ratelimit.envoy-gateway-system.svc.example-cluster.local:8081", got)
+	require.Equal(t, "grpc://envoy-ratelimit.envoy-gateway-system.svc.example-cluster.local:8081", got)
 }
diff --git a/internal/infrastructure/kubernetes/ratelimit/testdata/envoy-ratelimit-serviceaccount.yaml b/internal/infrastructure/kubernetes/ratelimit/testdata/envoy-ratelimit-serviceaccount.yaml
index c11aaac1e..86b4cfb20 100644
--- a/internal/infrastructure/kubernetes/ratelimit/testdata/envoy-ratelimit-serviceaccount.yaml
+++ b/internal/infrastructure/kubernetes/ratelimit/testdata/envoy-ratelimit-serviceaccount.yaml
@@ -5,7 +5,7 @@ metadata:
   name: envoy-ratelimit
   namespace: envoy-gateway-system
   ownerReferences:
-    - apiVersion: v1
-      kind: ServiceAccount
-      name: envoy-gateway
-      uid: test-owner-reference-uid-for-service-account
+  - apiVersion: v1
+    kind: ServiceAccount
+    name: envoy-gateway
+    uid: test-owner-reference-uid-for-service-account
diff --git a/internal/infrastructure/kubernetes/ratelimit/testdata/pdb/custom.yaml b/internal/infrastructure/kubernetes/ratelimit/testdata/pdb/custom.yaml
new file mode 100644
index 000000000..2899038da
--- /dev/null
+++ b/internal/infrastructure/kubernetes/ratelimit/testdata/pdb/custom.yaml
@@ -0,0 +1,27 @@
+apiVersion: policy/v1
+kind: PodDisruptionBudget
+metadata:
+  labels:
+    app.kubernetes.io/component: ratelimit
+    app.kubernetes.io/managed-by: envoy-gateway
+    app.kubernetes.io/name: envoy-ratelimit
+  name: envoy-ratelimit
+  namespace: envoy-gateway-system
+  ownerReferences:
+  - apiVersion: apps/v1
+    kind: Deployment
+    name: envoy-gateway
+    uid: test-owner-reference-uid-for-deployment
+spec:
+  maxUnavailable: 11
+  minAvailable: 10
+  selector:
+    matchLabels:
+      app.kubernetes.io/component: ratelimit
+      app.kubernetes.io/managed-by: envoy-gateway
+      app.kubernetes.io/name: envoy-ratelimit
+status:
+  currentHealthy: 0
+  desiredHealthy: 0
+  disruptionsAllowed: 0
+  expectedPods: 0
diff --git a/internal/infrastructure/kubernetes/ratelimit/testdata/pdb/default.yaml b/internal/infrastructure/kubernetes/ratelimit/testdata/pdb/default.yaml
new file mode 100644
index 000000000..4d7db02e7
--- /dev/null
+++ b/internal/infrastructure/kubernetes/ratelimit/testdata/pdb/default.yaml
@@ -0,0 +1,26 @@
+apiVersion: policy/v1
+kind: PodDisruptionBudget
+metadata:
+  labels:
+    app.kubernetes.io/component: ratelimit
+    app.kubernetes.io/managed-by: envoy-gateway
+    app.kubernetes.io/name: envoy-ratelimit
+  name: envoy-ratelimit
+  namespace: envoy-gateway-system
+  ownerReferences:
+  - apiVersion: apps/v1
+    kind: Deployment
+    name: envoy-gateway
+    uid: test-owner-reference-uid-for-deployment
+spec:
+  minAvailable: 0
+  selector:
+    matchLabels:
+      app.kubernetes.io/component: ratelimit
+      app.kubernetes.io/managed-by: envoy-gateway
+      app.kubernetes.io/name: envoy-ratelimit
+status:
+  currentHealthy: 0
+  desiredHealthy: 0
+  disruptionsAllowed: 0
+  expectedPods: 0
diff --git a/internal/infrastructure/kubernetes/ratelimit/testdata/pdb/with-name.yaml b/internal/infrastructure/kubernetes/ratelimit/testdata/pdb/with-name.yaml
new file mode 100644
index 000000000..b1aa798ae
--- /dev/null
+++ b/internal/infrastructure/kubernetes/ratelimit/testdata/pdb/with-name.yaml
@@ -0,0 +1,26 @@
+apiVersion: policy/v1
+kind: PodDisruptionBudget
+metadata:
+  labels:
+    app.kubernetes.io/component: ratelimit
+    app.kubernetes.io/managed-by: envoy-gateway
+    app.kubernetes.io/name: envoy-ratelimit
+  name: custom-pdb-name
+  namespace: envoy-gateway-system
+  ownerReferences:
+  - apiVersion: apps/v1
+    kind: Deployment
+    name: envoy-gateway
+    uid: test-owner-reference-uid-for-deployment
+spec:
+  minAvailable: 0
+  selector:
+    matchLabels:
+      app.kubernetes.io/component: ratelimit
+      app.kubernetes.io/managed-by: envoy-gateway
+      app.kubernetes.io/name: envoy-ratelimit
+status:
+  currentHealthy: 0
+  desiredHealthy: 0
+  disruptionsAllowed: 0
+  expectedPods: 0
diff --git a/internal/infrastructure/kubernetes/ratelimit/testdata/services/envoy-ratelimit-service-default.yaml b/internal/infrastructure/kubernetes/ratelimit/testdata/services/envoy-ratelimit-service-default.yaml
index be6bddc9a..a57933809 100644
--- a/internal/infrastructure/kubernetes/ratelimit/testdata/services/envoy-ratelimit-service-default.yaml
+++ b/internal/infrastructure/kubernetes/ratelimit/testdata/services/envoy-ratelimit-service-default.yaml
@@ -2,29 +2,31 @@ apiVersion: v1
 kind: Service
 metadata:
   labels:
-    app.kubernetes.io/name: envoy-ratelimit
     app.kubernetes.io/component: ratelimit
     app.kubernetes.io/managed-by: envoy-gateway
+    app.kubernetes.io/name: envoy-ratelimit
   name: envoy-ratelimit
   namespace: envoy-gateway-system
   ownerReferences:
-    - apiVersion: v1
-      kind: Service
-      name: envoy-gateway
-      uid: test-owner-reference-uid-for-service
+  - apiVersion: v1
+    kind: Service
+    name: envoy-gateway
+    uid: test-owner-reference-uid-for-service
 spec:
-  type: ClusterIP
-  sessionAffinity: None
   ports:
-    - name: http
-      port: 8081
-      protocol: TCP
-      targetPort: 8081
-    - name: metrics
-      port: 19001
-      protocol: TCP
-      targetPort: 19001
+  - name: http
+    port: 8081
+    protocol: TCP
+    targetPort: 8081
+  - name: metrics
+    port: 19001
+    protocol: TCP
+    targetPort: 19001
   selector:
-    app.kubernetes.io/name: envoy-ratelimit
     app.kubernetes.io/component: ratelimit
     app.kubernetes.io/managed-by: envoy-gateway
+    app.kubernetes.io/name: envoy-ratelimit
+  sessionAffinity: None
+  type: ClusterIP
+status:
+  loadBalancer: {}
diff --git a/internal/infrastructure/kubernetes/ratelimit/testdata/services/envoy-ratelimit-service-no-prometheus.yaml b/internal/infrastructure/kubernetes/ratelimit/testdata/services/envoy-ratelimit-service-no-prometheus.yaml
index 562d013ef..fd91b68cd 100644
--- a/internal/infrastructure/kubernetes/ratelimit/testdata/services/envoy-ratelimit-service-no-prometheus.yaml
+++ b/internal/infrastructure/kubernetes/ratelimit/testdata/services/envoy-ratelimit-service-no-prometheus.yaml
@@ -2,25 +2,27 @@ apiVersion: v1
 kind: Service
 metadata:
   labels:
-    app.kubernetes.io/name: envoy-ratelimit
     app.kubernetes.io/component: ratelimit
     app.kubernetes.io/managed-by: envoy-gateway
+    app.kubernetes.io/name: envoy-ratelimit
   name: envoy-ratelimit
   namespace: envoy-gateway-system
   ownerReferences:
-    - apiVersion: v1
-      kind: Service
-      name: envoy-gateway
-      uid: test-owner-reference-uid-for-service
+  - apiVersion: v1
+    kind: Service
+    name: envoy-gateway
+    uid: test-owner-reference-uid-for-service
 spec:
-  type: ClusterIP
-  sessionAffinity: None
   ports:
-    - name: http
-      port: 8081
-      protocol: TCP
-      targetPort: 8081
+  - name: http
+    port: 8081
+    protocol: TCP
+    targetPort: 8081
   selector:
-    app.kubernetes.io/name: envoy-ratelimit
     app.kubernetes.io/component: ratelimit
     app.kubernetes.io/managed-by: envoy-gateway
+    app.kubernetes.io/name: envoy-ratelimit
+  sessionAffinity: None
+  type: ClusterIP
+status:
+  loadBalancer: {}
diff --git a/internal/message/watchutil_internal_test.go b/internal/message/watchutil_internal_test.go
new file mode 100644
index 000000000..8bf56ab2e
--- /dev/null
+++ b/internal/message/watchutil_internal_test.go
@@ -0,0 +1,68 @@
+// Copyright Envoy Gateway Authors
+// SPDX-License-Identifier: Apache-2.0
+// The full text of the Apache license is available in the LICENSE file at
+// the root of the repo.
+
+package message
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/require"
+	"github.com/telepresenceio/watchable"
+)
+
+func TestCoalesceUpdates(t *testing.T) {
+	t.Parallel()
+
+	tests := []struct {
+		name     string
+		input    []watchable.Update[string, int]
+		expected []watchable.Update[string, int]
+	}{
+		{
+			name:     "empty input returns nil",
+			input:    []watchable.Update[string, int]{},
+			expected: []watchable.Update[string, int]{},
+		},
+		{
+			name: "simple updates without repeats",
+			input: []watchable.Update[string, int]{
+				{Key: "foo", Value: 1},
+				{Key: "bar", Value: 2},
+				{Key: "baz", Value: 3},
+			},
+			expected: []watchable.Update[string, int]{
+				{Key: "foo", Value: 1},
+				{Key: "bar", Value: 2},
+				{Key: "baz", Value: 3},
+			},
+		},
+		{
+			name: "latest update per key wins",
+			input: []watchable.Update[string, int]{
+				{Key: "foo", Value: 1},
+				{Key: "bar", Delete: true, Value: 2},
+				{Key: "baz", Value: 3},
+				{Key: "bar", Value: 4},
+				{Key: "foo", Value: 5},
+				{Key: "baz", Delete: true, Value: 6},
+				{Key: "bar", Value: 7},
+			},
+			expected: []watchable.Update[string, int]{
+				{Key: "foo", Value: 5},
+				{Key: "baz", Delete: true, Value: 6},
+				{Key: "bar", Value: 7},
+			},
+		},
+	}
+
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			t.Parallel()
+
+			actual := coalesceUpdates("test-runner", tc.input)
+			require.Equal(t, tc.expected, actual)
+		})
+	}
+}
diff --git a/internal/message/watchutil_test.go b/internal/message/watchutil_test.go
index bc6b10c9c..21411b3f6 100644
--- a/internal/message/watchutil_test.go
+++ b/internal/message/watchutil_test.go
@@ -89,7 +89,7 @@ func TestHandleSubscriptionAlreadyInitialized(t *testing.T) {
 			}
 		},
 	)
-	assert.Equal(t, 2, storeCalls)
+	assert.LessOrEqual(t, storeCalls, 2) // updates can be coalesced
 	assert.Equal(t, 1, deleteCalls)
 }
 
@@ -268,7 +268,11 @@ func TestControllerResourceUpdate(t *testing.T) {
 					m.GatewayAPIResources.Close()
 				}
 			})
-			assert.Equal(t, tc.updates, updates)
+			if tc.updates > 1 {
+				assert.LessOrEqual(t, updates, tc.updates) // Updates can be coalesced
+			} else {
+				assert.Equal(t, 1, updates)
+			}
 		})
 	}
 }
diff --git a/internal/xds/translator/testdata/in/ratelimit-config/method-match.yaml b/internal/xds/translator/testdata/in/ratelimit-config/method-match.yaml
new file mode 100644
index 000000000..c2055ed0d
--- /dev/null
+++ b/internal/xds/translator/testdata/in/ratelimit-config/method-match.yaml
@@ -0,0 +1,36 @@
+http:
+- name: "first-listener"
+  address: "0.0.0.0"
+  port: 10080
+  hostnames:
+  - "*"
+  path:
+    mergeSlashes: true
+    escapedSlashesAction: UnescapeAndRedirect
+  routes:
+  - name: "first-route"
+    traffic:
+      rateLimit:
+        global:
+          rules:
+          - name: "test-namespace/test-policy-1/rule/0"
+            methodMatches:
+            - exact: "GET"
+            - exact: "PUT"
+            limit:
+              requests: 5
+              unit: second
+          - name: "test-namespace/test-policy-1/rule/1"
+            methodMatches:
+            - exact: "POST"
+            limit:
+              requests: 50
+              unit: second
+    pathMatch:
+      exact: "foo/bar"
+    destination:
+      name: "first-route-dest"
+      settings:
+      - endpoints:
+        - host: "1.2.3.4"
+          port: 50000
diff --git a/internal/xds/translator/testdata/in/ratelimit-config/multiple-all-match.yaml b/internal/xds/translator/testdata/in/ratelimit-config/multiple-all-match.yaml
new file mode 100644
index 000000000..f69506bad
--- /dev/null
+++ b/internal/xds/translator/testdata/in/ratelimit-config/multiple-all-match.yaml
@@ -0,0 +1,45 @@
+http:
+- name: "first-listener"
+  address: "0.0.0.0"
+  port: 10080
+  hostnames:
+  - "*"
+  path:
+    mergeSlashes: true
+    escapedSlashesAction: UnescapeAndRedirect
+  routes:
+  - name: "first-route"
+    traffic:
+      rateLimit:
+        global:
+          rules:
+          - name: "test-namespace/test-policy-1/rule/0"
+            headerMatches:
+            - name: "x-user-id"
+              exact: "one"
+            - name: "x-user-id"
+              exact: "two"
+            - name: "x-org-id"
+              exact: "three"
+            cidrMatch:
+              cidr: 0.0.0.0/0
+              ip: 0.0.0.0
+              maskLen: 0
+              isIPv6: false
+              distinct: false
+            methodMatches:
+            - exact: "GET"
+            - exact: "POST"
+            pathMatch:
+              exact: "foo/bar/baz"
+            limit:
+              requests: 5
+              unit: second
+    pathMatch:
+      exact: "foo/bar"
+    destination:
+      name: "first-route-dest"
+      settings:
+      - endpoints:
+        - host: "1.2.3.4"
+          port: 50000
diff --git a/internal/xds/translator/testdata/in/ratelimit-config/path-match.yaml b/internal/xds/translator/testdata/in/ratelimit-config/path-match.yaml
new file mode 100644
index 000000000..252235ee4
--- /dev/null
+++ b/internal/xds/translator/testdata/in/ratelimit-config/path-match.yaml
@@ -0,0 +1,35 @@
+http:
+- name: "first-listener"
+  address: "0.0.0.0"
+  port: 10080
+  hostnames:
+  - "*"
+  path:
+    mergeSlashes: true
+    escapedSlashesAction: UnescapeAndRedirect
+  routes:
+  - name: "first-route"
+    traffic:
+      rateLimit:
+        global:
+          rules:
+          - name: "test-namespace/test-policy-1/rule/0"
+            pathMatch:
+              exact: "foo/bar"
+            limit:
+              requests: 5
+              unit: second
+          - name: "test-namespace/test-policy-1/rule/1"
+            pathMatch:
+              safeRegex: "foo/bar"
+            limit:
+              requests: 5
+              unit: second
+    pathMatch:
+      exact: "foo/bar"
+    destination:
+      name: "first-route-dest"
+      settings:
+      - endpoints:
+        - host: "1.2.3.4"
+          port: 50000
diff --git a/internal/xds/translator/testdata/in/xds-ir/ext-auth-http-retry.yaml b/internal/xds/translator/testdata/in/xds-ir/ext-auth-http-retry.yaml
new file mode 100644
index 000000000..7c1ae56de
--- /dev/null
+++ b/internal/xds/translator/testdata/in/xds-ir/ext-auth-http-retry.yaml
@@ -0,0 +1,178 @@
+http:
+  - address: 0.0.0.0
+    hostnames:
+      - '*'
+    isHTTP2: false
+    name: default/gateway-1/http
+    path:
+      escapedSlashesAction: UnescapeAndRedirect
+      mergeSlashes: true
+    port: 10080
+    routes:
+      - name: httproute/default/httproute-1/rule/0/match/0/www_foo_com
+        hostname: www.foo.com
+        isHTTP2: false
+        pathMatch:
+          distinct: false
+          name: ""
+          prefix: /foo1
+        backendWeights:
+          invalid: 0
+          valid: 0
+        destination:
+          name: httproute/default/httproute-1/rule/0
+          settings:
+            - addressType: IP
+              endpoints:
+                - host: 7.7.7.7
+                  port: 8080
+              protocol: HTTP
+              weight: 1
+              name: httproute/default/httproute-1/rule/0/backend/0
+        security:
+          extAuth:
+            name: securitypolicy/default/policy-for-http-route-1
+            traffic:
+              retry:
+                numRetries: 2
+                perRetry:
+                  backOff:
+                    baseInterval: 200ms
+                    maxInterval: 3s
+                  timeout: 3s
+                retryOn:
+                  triggers:
+                    - 5xx
+                    - deadline-exceeded
+                    - internal
+                    - unavailable
+            failOpen: false
+            http:
+              authority: http-backend.default:9000
+              destination:
+                name: securitypolicy/default/policy-for-http-route-1/default/http-backend
+                settings:
+                  - addressType: IP
+                    endpoints:
+                      - host: 8.8.4.4
+                        port: 9001
+                    protocol: HTTP
+                    weight: 1
+                    name: securitypolicy/default/policy-for-http-route-1/default/http-backend/backend/0
+                  - addressType: IP
+                    endpoints:
+                      - host: 8.8.8.8
+                        port: 9000
+                    protocol: HTTP
+                    weight: 1
+                    name: securitypolicy/default/policy-for-http-route-1/default/http-backend/backend/1
+            headersToExtAuth:
+              - header1
+              - header2
+      - name: httproute/default/httproute-1/rule/1/match/0/www_foo_com
+        hostname: www.foo.com
+        isHTTP2: false
+        pathMatch:
+          distinct: false
+          name: ""
+          prefix: /foo2
+        backendWeights:
+          invalid: 0
+          valid: 0
+        destination:
+          name: httproute/default/httproute-1/rule/1
+          settings:
+            - addressType: IP
+              endpoints:
+                - host: 7.7.7.7
+                  port: 8080
+              protocol: HTTP
+              weight: 1
+              name: httproute/default/httproute-1/rule/1/backend/0
+        security:
+          extAuth:
+            name: securitypolicy/default/policy-for-http-route-1
+            traffic:
+              retry:
+                numRetries: 2
+                perRetry:
+                  backOff:
+                    baseInterval: 200ms
+                    maxInterval: 3s
+                  timeout: 3s
+                retryOn:
+                  triggers:
+                    - 5xx
+                    - deadline-exceeded
+                    - internal
+                    - unavailable
+            failOpen: false
+            http:
+              authority: http-backend.default:9000
+              destination:
+                name: securitypolicy/default/policy-for-http-route-1/default/http-backend
+                settings:
+                  - addressType: IP
+                    endpoints:
+                      - host: 8.8.8.8
+                        port: 9000
+                    protocol: HTTP
+                    weight: 1
+                    name: securitypolicy/default/policy-for-http-route-1/default/http-backend/backend/0
+            headersToExtAuth:
+              - header1
+              - header2
+      - name: httproute/default/httproute-2/rule/0/match/0/www_bar_com
+        hostname: www.bar.com
+        isHTTP2: false
+        pathMatch:
+          distinct: false
+          name: ""
+          prefix: /bar
+        backendWeights:
+          invalid: 0
+          valid: 0
+        destination:
+          name: httproute/default/httproute-2/rule/0
+          settings:
+            - addressType: IP
+              endpoints:
+                - host: 7.7.7.7
+                  port: 8080
+              protocol: HTTP
+              weight: 1
+              name: httproute/default/httproute-2/rule/0/backend/0
+        security:
+          extAuth:
+            name: securitypolicy/default/policy-for-gateway-1
+            traffic:
+              retry:
+                numRetries: 2
+                perRetry:
+                  backOff:
+                    baseInterval: 200ms
+                    maxInterval: 3s
+                  timeout: 3s
+                retryOn:
+                  triggers:
+                    - 5xx
+                    - deadline-exceeded
+                    - internal
+                    - unavailable
+            failOpen: true
+            http:
+              authority: http-backend.envoy-gateway:80
+              destination:
+                name: securitypolicy/default/policy-for-gateway-1/envoy-gateway/http-backend
+                settings:
+                  - addressType: IP
+                    endpoints:
+                      - host: 7.7.7.7
+                        port: 80
+                    protocol: HTTP
+                    weight: 1
+                    name: securitypolicy/default/policy-for-gateway-1/envoy-gateway/http-backend/backend/0
+              headersToBackend:
+                - header1
+                - header2
+              path: /auth
diff --git a/internal/xds/translator/testdata/out/ratelimit-config/distinct-match.routes.yaml b/internal/xds/translator/testdata/out/ratelimit-config/distinct-match.routes.yaml
new file mode 100644
index 000000000..1337d191a
--- /dev/null
+++ b/internal/xds/translator/testdata/out/ratelimit-config/distinct-match.routes.yaml
@@ -0,0 +1,12 @@
+costSpecified: false
+rateLimits:
+- actions:
+  - ActionSpecifier:
+      GenericKey:
+        descriptor_key: first-route
+        descriptor_value: first-route
+  - ActionSpecifier:
+      RequestHeaders:
+        descriptor_key: rule-0-match-0
+        header_name: x-user-id
+routeName: first-route
diff --git a/internal/xds/translator/testdata/out/ratelimit-config/distinct-remote-address-match.routes.yaml b/internal/xds/translator/testdata/out/ratelimit-config/distinct-remote-address-match.routes.yaml
new file mode 100644
index 000000000..03b00685a
--- /dev/null
+++ b/internal/xds/translator/testdata/out/ratelimit-config/distinct-remote-address-match.routes.yaml
@@ -0,0 +1,14 @@
+costSpecified: false
+rateLimits:
+- actions:
+  - ActionSpecifier:
+      GenericKey:
+        descriptor_key: first-route
+        descriptor_value: first-route
+  - ActionSpecifier:
+      MaskedRemoteAddress:
+        v4_prefix_mask_len:
+          value: 16
+  - ActionSpecifier:
+      RemoteAddress: {}
+routeName: first-route
diff --git a/internal/xds/translator/testdata/out/ratelimit-config/empty-header-matches.routes.yaml b/internal/xds/translator/testdata/out/ratelimit-config/empty-header-matches.routes.yaml
new file mode 100644
index 000000000..395311aff
--- /dev/null
+++ b/internal/xds/translator/testdata/out/ratelimit-config/empty-header-matches.routes.yaml
@@ -0,0 +1,12 @@
+costSpecified: false
+rateLimits:
+- actions:
+  - ActionSpecifier:
+      GenericKey:
+        descriptor_key: first-route
+        descriptor_value: first-route
+  - ActionSpecifier:
+      GenericKey:
+        descriptor_key: rule-0-match--1
+        descriptor_value: rule-0-match--1
+routeName: first-route
diff --git a/internal/xds/translator/testdata/out/ratelimit-config/global-shared-distinct-match.routes.yaml b/internal/xds/translator/testdata/out/ratelimit-config/global-shared-distinct-match.routes.yaml
new file mode 100644
index 000000000..c0131d32a
--- /dev/null
+++ b/internal/xds/translator/testdata/out/ratelimit-config/global-shared-distinct-match.routes.yaml
@@ -0,0 +1,12 @@
+costSpecified: false
+rateLimits:
+- actions:
+  - ActionSpecifier:
+      GenericKey:
+        descriptor_key: test-namespace/test-policy-1/rule/0
+        descriptor_value: test-namespace/test-policy-1/rule/0
+  - ActionSpecifier:
+      RequestHeaders:
+        descriptor_key: rule-0-match-0
+        header_name: x-user-id
+routeName: first-route
diff --git a/internal/xds/translator/testdata/out/ratelimit-config/global-shared-multiple-shared-policies.routes.yaml b/internal/xds/translator/testdata/out/ratelimit-config/global-shared-multiple-shared-policies.routes.yaml
new file mode 100644
index 000000000..da98addba
--- /dev/null
+++ b/internal/xds/translator/testdata/out/ratelimit-config/global-shared-multiple-shared-policies.routes.yaml
@@ -0,0 +1,83 @@
+costSpecified: false
+rateLimits:
+- actions:
+  - ActionSpecifier:
+      GenericKey:
+        descriptor_key: test-namespace/test-policy-1/rule/0
+        descriptor_value: test-namespace/test-policy-1/rule/0
+  - ActionSpecifier:
+      HeaderValueMatch:
+        descriptor_key: rule-0-match-0
+        descriptor_value: rule-0-match-0
+        expect_match:
+          value: true
+        headers:
+        - HeaderMatchSpecifier:
+            StringMatch:
+              MatchPattern:
+                Exact: one
+          name: x-user-id
+routeName: first-route
+---
+costSpecified: false
+rateLimits:
+- actions:
+  - ActionSpecifier:
+      GenericKey:
+        descriptor_key: test-namespace/test-policy-1/rule/0
+        descriptor_value: test-namespace/test-policy-1/rule/0
+  - ActionSpecifier:
+      HeaderValueMatch:
+        descriptor_key: rule-0-match-0
+        descriptor_value: rule-0-match-0
+        expect_match:
+          value: true
+        headers:
+        - HeaderMatchSpecifier:
+            StringMatch:
+              MatchPattern:
+                Exact: one
+          name: x-user-id
+routeName: second-route
+---
+costSpecified: false
+rateLimits:
+- actions:
+  - ActionSpecifier:
+      GenericKey:
+        descriptor_key: test-namespace/test-policy-2/rule/0
+        descriptor_value: test-namespace/test-policy-2/rule/0
+  - ActionSpecifier:
+      HeaderValueMatch:
+        descriptor_key: rule-0-match-0
+        descriptor_value: rule-0-match-0
+        expect_match:
+          value: true
+        headers:
+        - HeaderMatchSpecifier:
+            StringMatch:
+              MatchPattern:
+                Exact: two
+          name: x-user-id
+routeName: third-route
+---
+costSpecified: false
+rateLimits:
+- actions:
+  - ActionSpecifier:
+      GenericKey:
+        descriptor_key: fourth-route
+        descriptor_value: fourth-route
+  - ActionSpecifier:
+      HeaderValueMatch:
+        descriptor_key: rule-0-match-0
+        descriptor_value: rule-0-match-0
+        expect_match:
+          value: true
+        headers:
+        - HeaderMatchSpecifier:
+            StringMatch:
+              MatchPattern:
+                Exact: three
+          name: x-user-id
+routeName: fourth-route
diff --git a/internal/xds/translator/testdata/out/ratelimit-config/header-and-cidr-matches.routes.yaml b/internal/xds/translator/testdata/out/ratelimit-config/header-and-cidr-matches.routes.yaml
new file mode 100644
index 000000000..1b3b97eee
--- /dev/null
+++ b/internal/xds/translator/testdata/out/ratelimit-config/header-and-cidr-matches.routes.yaml
@@ -0,0 +1,47 @@
+costSpecified: false
+rateLimits:
+- actions:
+  - ActionSpecifier:
+      GenericKey:
+        descriptor_key: first-route
+        descriptor_value: first-route
+  - ActionSpecifier:
+      HeaderValueMatch:
+        descriptor_key: rule-0-match-0
+        descriptor_value: rule-0-match-0
+        expect_match:
+          value: true
+        headers:
+        - HeaderMatchSpecifier:
+            StringMatch:
+              MatchPattern:
+                Exact: one
+          name: x-user-id
+  - ActionSpecifier:
+      HeaderValueMatch:
+        descriptor_key: rule-0-match-1
+        descriptor_value: rule-0-match-1
+        expect_match:
+          value: true
+        headers:
+        - HeaderMatchSpecifier:
+            StringMatch:
+              MatchPattern:
+                Exact: two
+          name: x-user-id
+  - ActionSpecifier:
+      HeaderValueMatch:
+        descriptor_key: rule-0-match-2
+        descriptor_value: rule-0-match-2
+        expect_match:
+          value: true
+        headers:
+        - HeaderMatchSpecifier:
+            StringMatch:
+              MatchPattern:
+                Exact: three
+          name: x-org-id
+  - ActionSpecifier:
+      MaskedRemoteAddress:
+        v4_prefix_mask_len: {}
+routeName: first-route
diff --git a/internal/xds/translator/testdata/out/ratelimit-config/masked-remote-address-match.routes.yaml b/internal/xds/translator/testdata/out/ratelimit-config/masked-remote-address-match.routes.yaml
new file mode 100644
index 000000000..ba1730475
--- /dev/null
+++ b/internal/xds/translator/testdata/out/ratelimit-config/masked-remote-address-match.routes.yaml
@@ -0,0 +1,12 @@
+costSpecified: false
+rateLimits:
+- actions:
+  - ActionSpecifier:
+      GenericKey:
+        descriptor_key: first-route
+        descriptor_value: first-route
+  - ActionSpecifier:
+      MaskedRemoteAddress:
+        v4_prefix_mask_len:
+          value: 16
+routeName: first-route
diff --git a/internal/xds/translator/testdata/out/ratelimit-config/method-match.routes.yaml b/internal/xds/translator/testdata/out/ratelimit-config/method-match.routes.yaml
new file mode 100644
index 000000000..0707f7ad1
--- /dev/null
+++ b/internal/xds/translator/testdata/out/ratelimit-config/method-match.routes.yaml
@@ -0,0 +1,54 @@
+costSpecified: false
+rateLimits:
+- actions:
+  - ActionSpecifier:
+      GenericKey:
+        descriptor_key: first-route
+        descriptor_value: first-route
+  - ActionSpecifier:
+      HeaderValueMatch:
+        descriptor_key: rule-0-method
+        descriptor_value: rule-0-method
+        expect_match:
+          value: true
+        headers:
+        - HeaderMatchSpecifier:
+            StringMatch:
+              MatchPattern:
+                Exact: GET
+          name: :method
+- actions:
+  - ActionSpecifier:
+      GenericKey:
+        descriptor_key: first-route
+        descriptor_value: first-route
+  - ActionSpecifier:
+      HeaderValueMatch:
+        descriptor_key: rule-0-method
+        descriptor_value: rule-0-method
+        expect_match:
+          value: true
+        headers:
+        - HeaderMatchSpecifier:
+            StringMatch:
+              MatchPattern:
+                Exact: PUT
+          name: :method
+- actions:
+  - ActionSpecifier:
+      GenericKey:
+        descriptor_key: first-route
+        descriptor_value: first-route
+  - ActionSpecifier:
+      HeaderValueMatch:
+        descriptor_key: rule-1-method
+        descriptor_value: rule-1-method
+        expect_match:
+          value: true
+        headers:
+        - HeaderMatchSpecifier:
+            StringMatch:
+              MatchPattern:
+                Exact: POST
+          name: :method
+routeName: first-route
diff --git a/internal/xds/translator/testdata/out/ratelimit-config/method-match.yaml b/internal/xds/translator/testdata/out/ratelimit-config/method-match.yaml
new file mode 100644
index 000000000..841f0e1c3
--- /dev/null
+++ b/internal/xds/translator/testdata/out/ratelimit-config/method-match.yaml
@@ -0,0 +1,31 @@
+name: first-listener
+domain: first-listener
+descriptors:
+  - key: first-route
+    value: first-route
+    rate_limit: null
+    descriptors:
+      - key: rule-0-method
+        value: rule-0-method
+        rate_limit:
+          requests_per_unit: 5
+          unit: SECOND
+          unlimited: false
+          name: ""
+          replaces: []
+        descriptors: []
+        shadow_mode: false
+        detailed_metric: false
+      - key: rule-1-method
+        value: rule-1-method
+        rate_limit:
+          requests_per_unit: 50
+          unit: SECOND
+          unlimited: false
+          name: ""
+          replaces: []
+        descriptors: []
+        shadow_mode: false
+        detailed_metric: false
+    shadow_mode: false
+    detailed_metric: false
diff --git a/internal/xds/translator/testdata/out/ratelimit-config/month-year-rule.routes.yaml b/internal/xds/translator/testdata/out/ratelimit-config/month-year-rule.routes.yaml
new file mode 100644
index 000000000..069bd860a
--- /dev/null
+++ b/internal/xds/translator/testdata/out/ratelimit-config/month-year-rule.routes.yaml
@@ -0,0 +1,41 @@
+costSpecified: false
+rateLimits:
+- actions:
+  - ActionSpecifier:
+      GenericKey:
+        descriptor_key: first-route
+        descriptor_value: first-route
+  - ActionSpecifier:
+      HeaderValueMatch:
+        descriptor_key: rule-0-match-0
+        descriptor_value: rule-0-match-0
+        expect_match:
+          value: true
+        headers:
+        - HeaderMatchSpecifier:
+            StringMatch:
+              MatchPattern:
+                Exact: one
+          name: x-user-id
+routeName: first-route
+---
+costSpecified: false
+rateLimits:
+- actions:
+  - ActionSpecifier:
+      GenericKey:
+        descriptor_key: second-route
+        descriptor_value: second-route
+  - ActionSpecifier:
+      HeaderValueMatch:
+        descriptor_key: rule-0-match-0
+        descriptor_value: rule-0-match-0
+        expect_match:
+          value: true
+        headers:
+        - HeaderMatchSpecifier:
+            StringMatch:
+              MatchPattern:
+                Exact: two
+          name: x-user-id
+routeName: second-route
diff --git a/internal/xds/translator/testdata/out/ratelimit-config/multiple-all-match.routes.yaml b/internal/xds/translator/testdata/out/ratelimit-config/multiple-all-match.routes.yaml
new file mode 100644
index 000000000..29c133f6e
--- /dev/null
+++ b/internal/xds/translator/testdata/out/ratelimit-config/multiple-all-match.routes.yaml
@@ -0,0 +1,139 @@
+costSpecified: false
+rateLimits:
+- actions:
+  - ActionSpecifier:
+      GenericKey:
+        descriptor_key: first-route
+        descriptor_value: first-route
+  - ActionSpecifier:
+      HeaderValueMatch:
+        descriptor_key: rule-0-match-0
+        descriptor_value: rule-0-match-0
+        expect_match:
+          value: true
+        headers:
+        - HeaderMatchSpecifier:
+            StringMatch:
+              MatchPattern:
+                Exact: one
+          name: x-user-id
+  - ActionSpecifier:
+      HeaderValueMatch:
+        descriptor_key: rule-0-match-1
+        descriptor_value: rule-0-match-1
+        expect_match:
+          value: true
+        headers:
+        - HeaderMatchSpecifier:
+            StringMatch:
+              MatchPattern:
+                Exact: two
+          name: x-user-id
+  - ActionSpecifier:
+      HeaderValueMatch:
+        descriptor_key: rule-0-match-2
+        descriptor_value: rule-0-match-2
+        expect_match:
+          value: true
+        headers:
+        - HeaderMatchSpecifier:
+            StringMatch:
+              MatchPattern:
+                Exact: three
+          name: x-org-id
+  - ActionSpecifier:
+      HeaderValueMatch:
+        descriptor_key: rule-0-method
+        descriptor_value: rule-0-method
+        expect_match:
+          value: true
+        headers:
+        - HeaderMatchSpecifier:
+            StringMatch:
+              MatchPattern:
+                Exact: GET
+          name: :method
+  - ActionSpecifier:
+      HeaderValueMatch:
+        descriptor_key: rule-0-path
+        descriptor_value: rule-0-path
+        expect_match:
+          value: true
+        headers:
+        - HeaderMatchSpecifier:
+            StringMatch:
+              MatchPattern:
+                Exact: foo/bar/baz
+          name: :path
+  - ActionSpecifier:
+      MaskedRemoteAddress:
+        v4_prefix_mask_len: {}
+- actions:
+  - ActionSpecifier:
+      GenericKey:
+        descriptor_key: first-route
+        descriptor_value: first-route
+  - ActionSpecifier:
+      HeaderValueMatch:
+        descriptor_key: rule-0-match-0
+        descriptor_value: rule-0-match-0
+        expect_match:
+          value: true
+        headers:
+        - HeaderMatchSpecifier:
+            StringMatch:
+              MatchPattern:
+                Exact: one
+          name: x-user-id
+  - ActionSpecifier:
+      HeaderValueMatch:
+        descriptor_key: rule-0-match-1
+        descriptor_value: rule-0-match-1
+        expect_match:
+          value: true
+        headers:
+        - HeaderMatchSpecifier:
+            StringMatch:
+              MatchPattern:
+                Exact: two
+          name: x-user-id
+  - ActionSpecifier:
+      HeaderValueMatch:
+        descriptor_key: rule-0-match-2
+        descriptor_value: rule-0-match-2
+        expect_match:
+          value: true
+        headers:
+        - HeaderMatchSpecifier:
+            StringMatch:
+              MatchPattern:
+                Exact: three
+          name: x-org-id
+  - ActionSpecifier:
+      HeaderValueMatch:
+        descriptor_key: rule-0-method
+        descriptor_value: rule-0-method
+        expect_match:
+          value: true
+        headers:
+        - HeaderMatchSpecifier:
+            StringMatch:
+              MatchPattern:
+                Exact: POST
+          name: :method
+  - ActionSpecifier:
+      HeaderValueMatch:
+        descriptor_key: rule-0-path
+        descriptor_value: rule-0-path
+        expect_match:
+          value: true
+        headers:
+        - HeaderMatchSpecifier:
+            StringMatch:
+              MatchPattern:
+                Exact: foo/bar/baz
+          name: :path
+  - ActionSpecifier:
+      MaskedRemoteAddress:
+        v4_prefix_mask_len: {}
+routeName: first-route
diff --git a/internal/xds/translator/testdata/out/ratelimit-config/multiple-all-match.yaml b/internal/xds/translator/testdata/out/ratelimit-config/multiple-all-match.yaml
new file mode 100644
index 000000000..ed7e36ad7
--- /dev/null
+++ b/internal/xds/translator/testdata/out/ratelimit-config/multiple-all-match.yaml
@@ -0,0 +1,50 @@
+name: first-listener
+domain: first-listener
+descriptors:
+  - key: first-route
+    value: first-route
+    rate_limit: null
+    descriptors:
+      - key: rule-0-match-0
+        value: rule-0-match-0
+        rate_limit: null
+        descriptors:
+          - key: rule-0-match-1
+            value: rule-0-match-1
+            rate_limit: null
+            descriptors:
+              - key: rule-0-match-2
+                value: rule-0-match-2
+                rate_limit: null
+                descriptors:
+                  - key: rule-0-method
+                    value: rule-0-method
+                    rate_limit: null
+                    descriptors:
+                      - key: rule-0-path
+                        value: rule-0-path
+                        rate_limit: null
+                        descriptors:
+                          - key: masked_remote_address
+                            value: 0.0.0.0/0
+                            rate_limit:
+                              requests_per_unit: 5
+                              unit: SECOND
+                              unlimited: false
+                              name: ""
+                              replaces: []
+                            descriptors: []
+                            shadow_mode: false
+                            detailed_metric: false
+                        shadow_mode: false
+                        detailed_metric: false
+                    shadow_mode: false
+                    detailed_metric: false
+                shadow_mode: false
+                detailed_metric: false
+            shadow_mode: false
+            detailed_metric: false
+        shadow_mode: false
+        detailed_metric: false
+    shadow_mode: false
+    detailed_metric: false
diff --git a/internal/xds/translator/testdata/out/ratelimit-config/multiple-domains.routes.yaml b/internal/xds/translator/testdata/out/ratelimit-config/multiple-domains.routes.yaml
new file mode 100644
index 000000000..8ea694376
--- /dev/null
+++ b/internal/xds/translator/testdata/out/ratelimit-config/multiple-domains.routes.yaml
@@ -0,0 +1,41 @@
+costSpecified: false
+rateLimits:
+- actions:
+  - ActionSpecifier:
+      GenericKey:
+        descriptor_key: test-namespace/test-policy-1/rule/0
+        descriptor_value: test-namespace/test-policy-1/rule/0
+  - ActionSpecifier:
+      HeaderValueMatch:
+        descriptor_key: rule-0-match-0
+        descriptor_value: rule-0-match-0
+        expect_match:
+          value: true
+        headers:
+        - HeaderMatchSpecifier:
+            StringMatch:
+              MatchPattern:
+                Exact: one
+          name: x-user-id
+routeName: first-route
+---
+costSpecified: false
+rateLimits:
+- actions:
+  - ActionSpecifier:
+      GenericKey:
+        descriptor_key: second-route
+        descriptor_value: second-route
+  - ActionSpecifier:
+      HeaderValueMatch:
+        descriptor_key: rule-0-match-0
+        descriptor_value: rule-0-match-0
+        expect_match:
+          value: true
+        headers:
+        - HeaderMatchSpecifier:
+            StringMatch:
+              MatchPattern:
+                Exact: one
+          name: x-user-id
+routeName: second-route
diff --git a/internal/xds/translator/testdata/out/ratelimit-config/multiple-global-shared-distinct-match.routes.yaml b/internal/xds/translator/testdata/out/ratelimit-config/multiple-global-shared-distinct-match.routes.yaml
new file mode 100644
index 000000000..219d19351
--- /dev/null
+++ b/internal/xds/translator/testdata/out/ratelimit-config/multiple-global-shared-distinct-match.routes.yaml
@@ -0,0 +1,30 @@
+costSpecified: false
+rateLimits:
+- actions:
+  - ActionSpecifier:
+      GenericKey:
+        descriptor_key: test-namespace/test-policy-1/rule/0
+        descriptor_value: test-namespace/test-policy-1/rule/0
+  - ActionSpecifier:
+      RequestHeaders:
+        descriptor_key: rule-0-match-0
+        header_name: x-user-id
+- actions:
+  - ActionSpecifier:
+      GenericKey:
+        descriptor_key: test-namespace/test-policy-1/rule/1
+        descriptor_value: test-namespace/test-policy-1/rule/1
+  - ActionSpecifier:
+      RequestHeaders:
+        descriptor_key: rule-1-match-0
+        header_name: x-user-id
+- actions:
+  - ActionSpecifier:
+      GenericKey:
+        descriptor_key: test-namespace/test-policy-1/rule/2
+        descriptor_value: test-namespace/test-policy-1/rule/2
+  - ActionSpecifier:
+      RequestHeaders:
+        descriptor_key: rule-2-match-0
+        header_name: x-user-id
+routeName: first-route
diff --git a/internal/xds/translator/testdata/out/ratelimit-config/multiple-listeners-distinct-match.routes.yaml b/internal/xds/translator/testdata/out/ratelimit-config/multiple-listeners-distinct-match.routes.yaml
new file mode 100644
index 000000000..576b6000a
--- /dev/null
+++ b/internal/xds/translator/testdata/out/ratelimit-config/multiple-listeners-distinct-match.routes.yaml
@@ -0,0 +1,25 @@
+costSpecified: false
+rateLimits:
+- actions:
+  - ActionSpecifier:
+      GenericKey:
+        descriptor_key: first-route
+        descriptor_value: first-route
+  - ActionSpecifier:
+      RequestHeaders:
+        descriptor_key: rule-0-match-0
+        header_name: x-user-id
+routeName: first-route
+---
+costSpecified: false
+rateLimits:
+- actions:
+  - ActionSpecifier:
+      GenericKey:
+        descriptor_key: first-route
+        descriptor_value: first-route
+  - ActionSpecifier:
+      RequestHeaders:
+        descriptor_key: rule-0-match-0
+        header_name: x-user-id
+routeName: first-route
diff --git a/internal/xds/translator/testdata/out/ratelimit-config/multiple-listeners-same-shared-distinct-match.routes.yaml b/internal/xds/translator/testdata/out/ratelimit-config/multiple-listeners-same-shared-distinct-match.routes.yaml
new file mode 100644
index 000000000..73e2bd199
--- /dev/null
+++ b/internal/xds/translator/testdata/out/ratelimit-config/multiple-listeners-same-shared-distinct-match.routes.yaml
@@ -0,0 +1,25 @@
+costSpecified: false
+rateLimits:
+- actions:
+  - ActionSpecifier:
+      GenericKey:
+        descriptor_key: test-namespace/test-policy-1/rule/0
+        descriptor_value: test-namespace/test-policy-1/rule/0
+  - ActionSpecifier:
+      RequestHeaders:
+        descriptor_key: rule-0-match-0
+        header_name: x-user-id
+routeName: first-route
+---
+costSpecified: false
+rateLimits:
+- actions:
+  - ActionSpecifier:
+      GenericKey:
+        descriptor_key: test-namespace/test-policy-1/rule/0
+        descriptor_value: test-namespace/test-policy-1/rule/0
+  - ActionSpecifier:
+      RequestHeaders:
+        descriptor_key: rule-0-match-0
+        header_name: x-user-id
+routeName: first-route
diff --git a/internal/xds/translator/testdata/out/ratelimit-config/multiple-listeners-shared-distinct-match.routes.yaml b/internal/xds/translator/testdata/out/ratelimit-config/multiple-listeners-shared-distinct-match.routes.yaml
new file mode 100644
index 000000000..842cd0535
--- /dev/null
+++ b/internal/xds/translator/testdata/out/ratelimit-config/multiple-listeners-shared-distinct-match.routes.yaml
@@ -0,0 +1,25 @@
+costSpecified: false
+rateLimits:
+- actions:
+  - ActionSpecifier:
+      GenericKey:
+        descriptor_key: first-route
+        descriptor_value: first-route
+  - ActionSpecifier:
+      RequestHeaders:
+        descriptor_key: rule-0-match-0
+        header_name: x-user-id
+routeName: first-route
+---
+costSpecified: false
+rateLimits:
+- actions:
+  - ActionSpecifier:
+      GenericKey:
+        descriptor_key: test-namespace/test-policy-2/rule/0
+        descriptor_value: test-namespace/test-policy-2/rule/0
+  - ActionSpecifier:
+      RequestHeaders:
+        descriptor_key: rule-0-match-0
+        header_name: x-user-id
+routeName: first-route
diff --git a/internal/xds/translator/testdata/out/ratelimit-config/multiple-masked-remote-address-match-with-same-cidr.routes.yaml b/internal/xds/translator/testdata/out/ratelimit-config/multiple-masked-remote-address-match-with-same-cidr.routes.yaml
new file mode 100644
index 000000000..9cc223dd9
--- /dev/null
+++ b/internal/xds/translator/testdata/out/ratelimit-config/multiple-masked-remote-address-match-with-same-cidr.routes.yaml
@@ -0,0 +1,25 @@
+costSpecified: false
+rateLimits:
+- actions:
+  - ActionSpecifier:
+      GenericKey:
+        descriptor_key: first-route
+        descriptor_value: first-route
+  - ActionSpecifier:
+      MaskedRemoteAddress:
+        v4_prefix_mask_len:
+          value: 32
+routeName: first-route
+---
+costSpecified: false
+rateLimits:
+- actions:
+  - ActionSpecifier:
+      GenericKey:
+        descriptor_key: second-route
+        descriptor_value: second-route
+  - ActionSpecifier:
+      MaskedRemoteAddress:
+        v4_prefix_mask_len:
+          value: 32
+routeName: second-route
diff --git a/internal/xds/translator/testdata/out/ratelimit-config/multiple-matches.routes.yaml b/internal/xds/translator/testdata/out/ratelimit-config/multiple-matches.routes.yaml
new file mode 100644
index 000000000..f0d15a74d
--- /dev/null
+++ b/internal/xds/translator/testdata/out/ratelimit-config/multiple-matches.routes.yaml
@@ -0,0 +1,32 @@
+costSpecified: false
+rateLimits:
+- actions:
+  - ActionSpecifier:
+      GenericKey:
+        descriptor_key: first-route
+        descriptor_value: first-route
+  - ActionSpecifier:
+      HeaderValueMatch:
+        descriptor_key: rule-0-match-0
+        descriptor_value: rule-0-match-0
+        expect_match:
+          value: true
+        headers:
+        - HeaderMatchSpecifier:
+            StringMatch:
+              MatchPattern:
+                Exact: one
+          name: x-user-id
+  - ActionSpecifier:
+      HeaderValueMatch:
+        descriptor_key: rule-0-match-1
+        descriptor_value: rule-0-match-1
+        expect_match:
+          value: true
+        headers:
+        - HeaderMatchSpecifier:
+            StringMatch:
+              MatchPattern:
+                Exact: two
+          name: x-user-id
+routeName: first-route
diff --git a/internal/xds/translator/testdata/out/ratelimit-config/multiple-routes.routes.yaml b/internal/xds/translator/testdata/out/ratelimit-config/multiple-routes.routes.yaml
new file mode 100644
index 000000000..069bd860a
--- /dev/null
+++ b/internal/xds/translator/testdata/out/ratelimit-config/multiple-routes.routes.yaml
@@ -0,0 +1,41 @@
+costSpecified: false
+rateLimits:
+- actions:
+  - ActionSpecifier:
+      GenericKey:
+        descriptor_key: first-route
+        descriptor_value: first-route
+  - ActionSpecifier:
+      HeaderValueMatch:
+        descriptor_key: rule-0-match-0
+        descriptor_value: rule-0-match-0
+        expect_match:
+          value: true
+        headers:
+        - HeaderMatchSpecifier:
+            StringMatch:
+              MatchPattern:
+                Exact: one
+          name: x-user-id
+routeName: first-route
+---
+costSpecified: false
+rateLimits:
+- actions:
+  - ActionSpecifier:
+      GenericKey:
+        descriptor_key: second-route
+        descriptor_value: second-route
+  - ActionSpecifier:
+      HeaderValueMatch:
+        descriptor_key: rule-0-match-0
+        descriptor_value: rule-0-match-0
+        expect_match:
+          value: true
+        headers:
+        - HeaderMatchSpecifier:
+            StringMatch:
+              MatchPattern:
+                Exact: two
+          name: x-user-id
+routeName: second-route
diff --git a/internal/xds/translator/testdata/out/ratelimit-config/multiple-rules.routes.yaml b/internal/xds/translator/testdata/out/ratelimit-config/multiple-rules.routes.yaml
new file mode 100644
index 000000000..335e342c8
--- /dev/null
+++ b/internal/xds/translator/testdata/out/ratelimit-config/multiple-rules.routes.yaml
@@ -0,0 +1,37 @@
+costSpecified: false
+rateLimits:
+- actions:
+  - ActionSpecifier:
+      GenericKey:
+        descriptor_key: first-route
+        descriptor_value: first-route
+  - ActionSpecifier:
+      HeaderValueMatch:
+        descriptor_key: rule-0-match-0
+        descriptor_value: rule-0-match-0
+        expect_match:
+          value: true
+        headers:
+        - HeaderMatchSpecifier:
+            StringMatch:
+              MatchPattern:
+                Exact: one
+          name: x-user-id
+- actions:
+  - ActionSpecifier:
+      GenericKey:
+        descriptor_key: first-route
+        descriptor_value: first-route
+  - ActionSpecifier:
+      HeaderValueMatch:
+        descriptor_key: rule-1-match-0
+        descriptor_value: rule-1-match-0
+        expect_match:
+          value: true
+        headers:
+        - HeaderMatchSpecifier:
+            StringMatch:
+              MatchPattern:
+                Exact: two
+          name: x-user-id
+routeName: first-route
diff --git a/internal/xds/translator/testdata/out/ratelimit-config/multiple-shared-and-unshared.routes.yaml b/internal/xds/translator/testdata/out/ratelimit-config/multiple-shared-and-unshared.routes.yaml
new file mode 100644
index 000000000..229797a7b
--- /dev/null
+++ b/internal/xds/translator/testdata/out/ratelimit-config/multiple-shared-and-unshared.routes.yaml
@@ -0,0 +1,71 @@
+costSpecified: false
+rateLimits:
+- actions:
+  - ActionSpecifier:
+      GenericKey:
+        descriptor_key: test-namespace/test-policy-1/rule/0
+        descriptor_value: test-namespace/test-policy-1/rule/0
+  - ActionSpecifier:
+      HeaderValueMatch:
+        descriptor_key: rule-0-match-0
+        descriptor_value: rule-0-match-0
+        expect_match:
+          value: true
+        headers:
+        - HeaderMatchSpecifier:
+            StringMatch:
+              MatchPattern:
+                Exact: one
+          name: x-user-id
+- actions:
+  - ActionSpecifier:
+      GenericKey:
+        descriptor_key: first-route
+        descriptor_value: first-route
+  - ActionSpecifier:
+      HeaderValueMatch:
+        descriptor_key: rule-0-match-0
+        descriptor_value: rule-0-match-0
+        expect_match:
+          value: true
+        headers:
+        - HeaderMatchSpecifier:
+            StringMatch:
+              MatchPattern:
+                Exact: two
+          name: x-user-id
+- actions:
+  - ActionSpecifier:
+      GenericKey:
+        descriptor_key: test-namespace/test-policy-1/rule/2
+        descriptor_value: test-namespace/test-policy-1/rule/2
+  - ActionSpecifier:
+      HeaderValueMatch:
+        descriptor_key: rule-1-match-0
+        descriptor_value: rule-1-match-0
+        expect_match:
+          value: true
+        headers:
+        - HeaderMatchSpecifier:
+            StringMatch:
+              MatchPattern:
+                Exact: three
+          name: x-user-id
+- actions:
+  - ActionSpecifier:
+      GenericKey:
+        descriptor_key: first-route
+        descriptor_value: first-route
+  - ActionSpecifier:
+      HeaderValueMatch:
+        descriptor_key: rule-1-match-0
+        descriptor_value: rule-1-match-0
+        expect_match:
+          value: true
+        headers:
+        - HeaderMatchSpecifier:
+            StringMatch:
+              MatchPattern:
+                Exact: four
+          name: x-user-id
+routeName: first-route
diff --git a/internal/xds/translator/testdata/out/ratelimit-config/multiple-shared-ratelimit-rules.routes.yaml b/internal/xds/translator/testdata/out/ratelimit-config/multiple-shared-ratelimit-rules.routes.yaml
new file mode 100644
index 000000000..0ef9a5b42
--- /dev/null
+++ b/internal/xds/translator/testdata/out/ratelimit-config/multiple-shared-ratelimit-rules.routes.yaml
@@ -0,0 +1,54 @@
+costSpecified: false
+rateLimits:
+- actions:
+  - ActionSpecifier:
+      GenericKey:
+        descriptor_key: test-namespace/test-policy-1/rule/0
+        descriptor_value: test-namespace/test-policy-1/rule/0
+  - ActionSpecifier:
+      HeaderValueMatch:
+        descriptor_key: rule-0-match-0
+        descriptor_value: rule-0-match-0
+        expect_match:
+          value: true
+        headers:
+        - HeaderMatchSpecifier:
+            StringMatch:
+              MatchPattern:
+                Exact: one
+          name: x-user-id
+- actions:
+  - ActionSpecifier:
+      GenericKey:
+        descriptor_key: test-namespace/test-policy-1/rule/1
+        descriptor_value: test-namespace/test-policy-1/rule/1
+  - ActionSpecifier:
+      HeaderValueMatch:
+        descriptor_key: rule-1-match-0
+        descriptor_value: rule-1-match-0
+        expect_match:
+          value: true
+        headers:
+        - HeaderMatchSpecifier:
+            StringMatch:
+              MatchPattern:
+                Exact: two
+          name: x-user-id
+- actions:
+  - ActionSpecifier:
+      GenericKey:
+        descriptor_key: first-route
+        descriptor_value: first-route
+  - ActionSpecifier:
+      HeaderValueMatch:
+        descriptor_key: rule-0-match-0
+        descriptor_value: rule-0-match-0
+        expect_match:
+          value: true
+        headers:
+        - HeaderMatchSpecifier:
+            StringMatch:
+              MatchPattern:
+                Exact: three
+          name: x-user-id
+routeName: first-route
diff --git a/internal/xds/translator/testdata/out/ratelimit-config/path-match.routes.yaml b/internal/xds/translator/testdata/out/ratelimit-config/path-match.routes.yaml
new file mode 100644
index 000000000..2bb598f5e
--- /dev/null
+++ b/internal/xds/translator/testdata/out/ratelimit-config/path-match.routes.yaml
@@ -0,0 +1,39 @@
+costSpecified: false
+rateLimits:
+- actions:
+  - ActionSpecifier:
+      GenericKey:
+        descriptor_key: first-route
+        descriptor_value: first-route
+  - ActionSpecifier:
+      HeaderValueMatch:
+        descriptor_key: rule-0-path
+        descriptor_value: rule-0-path
+        expect_match:
+          value: true
+        headers:
+        - HeaderMatchSpecifier:
+            StringMatch:
+              MatchPattern:
+                Exact: foo/bar
+          name: :path
+- actions:
+  - ActionSpecifier:
+      GenericKey:
+        descriptor_key: first-route
+        descriptor_value: first-route
+  - ActionSpecifier:
+      HeaderValueMatch:
+        descriptor_key: rule-1-path
+        descriptor_value: rule-1-path
+        expect_match:
+          value: true
+        headers:
+        - HeaderMatchSpecifier:
+            StringMatch:
+              MatchPattern:
+                SafeRegex:
+                  EngineType: null
+                  regex: foo/bar
+          name: :path
+routeName: first-route
diff --git a/internal/xds/translator/testdata/out/ratelimit-config/path-match.yaml b/internal/xds/translator/testdata/out/ratelimit-config/path-match.yaml
new file mode 100644
index 000000000..4cbb6d7b1
--- /dev/null
+++ b/internal/xds/translator/testdata/out/ratelimit-config/path-match.yaml
@@ -0,0 +1,31 @@
+name: first-listener
+domain: first-listener
+descriptors:
+  - key: first-route
+    value: first-route
+    rate_limit: null
+    descriptors:
+      - key: rule-0-path
+        value: rule-0-path
+        rate_limit:
+          requests_per_unit: 5
+          unit: SECOND
+          unlimited: false
+          name: ""
+          replaces: []
+        descriptors: []
+        shadow_mode: false
+        detailed_metric: false
+      - key: rule-1-path
+        value: rule-1-path
+        rate_limit:
+          requests_per_unit: 5
+          unit: SECOND
+          unlimited: false
+          name: ""
+          replaces: []
+        descriptors: []
+        shadow_mode: false
+        detailed_metric: false
+    shadow_mode: false
+    detailed_metric: false
diff --git a/internal/xds/translator/testdata/out/ratelimit-config/value-match.routes.yaml b/internal/xds/translator/testdata/out/ratelimit-config/value-match.routes.yaml
new file mode 100644
index 000000000..4686a1bd8
--- /dev/null
+++ b/internal/xds/translator/testdata/out/ratelimit-config/value-match.routes.yaml
@@ -0,0 +1,20 @@
+costSpecified: false
+rateLimits:
+- actions:
+  - ActionSpecifier:
+      GenericKey:
+        descriptor_key: first-route
+        descriptor_value: first-route
+  - ActionSpecifier:
+      HeaderValueMatch:
+        descriptor_key: rule-0-match-0
+        descriptor_value: rule-0-match-0
+        expect_match:
+          value: true
+        headers:
+        - HeaderMatchSpecifier:
+            StringMatch:
+              MatchPattern:
+                Exact: one
+          name: x-user-id
+routeName: first-route
diff --git a/internal/xds/translator/testdata/out/xds-ir/ext-auth-http-retry.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/ext-auth-http-retry.clusters.yaml
new file mode 100644
index 000000000..dcaf5cc85
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/ext-auth-http-retry.clusters.yaml
@@ -0,0 +1,120 @@
+- circuitBreakers:
+    thresholds:
+    - maxRetries: 1024
+  commonLbConfig: {}
+  connectTimeout: 10s
+  dnsLookupFamily: V4_PREFERRED
+  edsClusterConfig:
+    edsConfig:
+      ads: {}
+      resourceApiVersion: V3
+    serviceName: httproute/default/httproute-1/rule/0
+  ignoreHealthOnHostRemoval: true
+  lbPolicy: LEAST_REQUEST
+  loadBalancingPolicy:
+    policies:
+    - typedExtensionConfig:
+        name: envoy.load_balancing_policies.least_request
+        typedConfig:
+          '@type': type.googleapis.com/envoy.extensions.load_balancing_policies.least_request.v3.LeastRequest
+          localityLbConfig:
+            localityWeightedLbConfig: {}
+  name: httproute/default/httproute-1/rule/0
+  perConnectionBufferLimitBytes: 32768
+  type: EDS
+- circuitBreakers:
+    thresholds:
+    - maxRetries: 1024
+  commonLbConfig: {}
+  connectTimeout: 10s
+  dnsLookupFamily: V4_PREFERRED
+  edsClusterConfig:
+    edsConfig:
+      ads: {}
+      resourceApiVersion: V3
+    serviceName: httproute/default/httproute-1/rule/1
+  ignoreHealthOnHostRemoval: true
+  lbPolicy: LEAST_REQUEST
+  loadBalancingPolicy:
+    policies:
+    - typedExtensionConfig:
+        name: envoy.load_balancing_policies.least_request
+        typedConfig:
+          '@type': type.googleapis.com/envoy.extensions.load_balancing_policies.least_request.v3.LeastRequest
+          localityLbConfig:
+            localityWeightedLbConfig: {}
+  name: httproute/default/httproute-1/rule/1
+  perConnectionBufferLimitBytes: 32768
+  type: EDS
+- circuitBreakers:
+    thresholds:
+    - maxRetries: 1024
+  commonLbConfig: {}
+  connectTimeout: 10s
+  dnsLookupFamily: V4_PREFERRED
+  edsClusterConfig:
+    edsConfig:
+      ads: {}
+      resourceApiVersion: V3
+    serviceName: httproute/default/httproute-2/rule/0
+  ignoreHealthOnHostRemoval: true
+  lbPolicy: LEAST_REQUEST
+  loadBalancingPolicy:
+    policies:
+    - typedExtensionConfig:
+        name: envoy.load_balancing_policies.least_request
+        typedConfig:
+          '@type': type.googleapis.com/envoy.extensions.load_balancing_policies.least_request.v3.LeastRequest
+          localityLbConfig:
+            localityWeightedLbConfig: {}
+  name: httproute/default/httproute-2/rule/0
+  perConnectionBufferLimitBytes: 32768
+  type: EDS
+- circuitBreakers:
+    thresholds:
+    - maxRetries: 1024
+  commonLbConfig: {}
+  connectTimeout: 10s
+  dnsLookupFamily: V4_PREFERRED
+  edsClusterConfig:
+    edsConfig:
+      ads: {}
+      resourceApiVersion: V3
+    serviceName: securitypolicy/default/policy-for-http-route-1/default/http-backend
+  ignoreHealthOnHostRemoval: true
+  lbPolicy: LEAST_REQUEST
+  loadBalancingPolicy:
+    policies:
+    - typedExtensionConfig:
+        name: envoy.load_balancing_policies.least_request
+        typedConfig:
+          '@type': type.googleapis.com/envoy.extensions.load_balancing_policies.least_request.v3.LeastRequest
+          localityLbConfig:
+            localityWeightedLbConfig: {}
+  name: securitypolicy/default/policy-for-http-route-1/default/http-backend
+  perConnectionBufferLimitBytes: 32768
+  type: EDS
+- circuitBreakers:
+    thresholds:
+    - maxRetries: 1024
+  commonLbConfig: {}
+  connectTimeout: 10s
+  dnsLookupFamily: V4_PREFERRED
+  edsClusterConfig:
+    edsConfig:
+      ads: {}
+      resourceApiVersion: V3
+    serviceName: securitypolicy/default/policy-for-gateway-1/envoy-gateway/http-backend
+  ignoreHealthOnHostRemoval: true
+  lbPolicy: LEAST_REQUEST
+  loadBalancingPolicy:
+    policies:
+    - typedExtensionConfig:
+        name: envoy.load_balancing_policies.least_request
+        typedConfig:
+          '@type': type.googleapis.com/envoy.extensions.load_balancing_policies.least_request.v3.LeastRequest
+          localityLbConfig:
+            localityWeightedLbConfig: {}
+  name: securitypolicy/default/policy-for-gateway-1/envoy-gateway/http-backend
+  perConnectionBufferLimitBytes: 32768
+  type: EDS
diff --git a/internal/xds/translator/testdata/out/xds-ir/ext-auth-http-retry.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/ext-auth-http-retry.endpoints.yaml
new file mode 100644
index 000000000..ec38cf183
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/ext-auth-http-retry.endpoints.yaml
@@ -0,0 +1,70 @@
+- clusterName: httproute/default/httproute-1/rule/0
+  endpoints:
+  - lbEndpoints:
+    - endpoint:
+        address:
+          socketAddress:
+            address: 7.7.7.7
+            portValue: 8080
+      loadBalancingWeight: 1
+    loadBalancingWeight: 1
+    locality:
+      region: httproute/default/httproute-1/rule/0/backend/0
+- clusterName: httproute/default/httproute-1/rule/1
+  endpoints:
+  - lbEndpoints:
+    - endpoint:
+        address:
+          socketAddress:
+            address: 7.7.7.7
+            portValue: 8080
+      loadBalancingWeight: 1
+    loadBalancingWeight: 1
+    locality:
+      region: httproute/default/httproute-1/rule/1/backend/0
+- clusterName: httproute/default/httproute-2/rule/0
+  endpoints:
+  - lbEndpoints:
+    - endpoint:
+        address:
+          socketAddress:
+            address: 7.7.7.7
+            portValue: 8080
+      loadBalancingWeight: 1
+    loadBalancingWeight: 1
+    locality:
+      region: httproute/default/httproute-2/rule/0/backend/0
+- clusterName: securitypolicy/default/policy-for-http-route-1/default/http-backend
+  endpoints:
+  - lbEndpoints:
+    - endpoint:
+        address:
+          socketAddress:
+            address: 8.8.4.4
+            portValue: 9001
+      loadBalancingWeight: 1
+    loadBalancingWeight: 1
+    locality:
+      region: securitypolicy/default/policy-for-http-route-1/default/http-backend/backend/0
+  - lbEndpoints:
+    - endpoint:
+        address:
+          socketAddress:
+            address: 8.8.8.8
+            portValue: 9000
+      loadBalancingWeight: 1
+    loadBalancingWeight: 1
+    locality:
+      region: securitypolicy/default/policy-for-http-route-1/default/http-backend/backend/1
+- clusterName: securitypolicy/default/policy-for-gateway-1/envoy-gateway/http-backend
+  endpoints:
+  - lbEndpoints:
+    - endpoint:
+        address:
+          socketAddress:
+            address: 7.7.7.7
+            portValue: 80
+      loadBalancingWeight: 1
+    loadBalancingWeight: 1
+    locality:
+      region: securitypolicy/default/policy-for-gateway-1/envoy-gateway/http-backend/backend/0
diff --git a/internal/xds/translator/testdata/out/xds-ir/ext-auth-http-retry.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/ext-auth-http-retry.listeners.yaml
new file mode 100644
index 000000000..6780d9706
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/ext-auth-http-retry.listeners.yaml
@@ -0,0 +1,82 @@
+- address:
+    socketAddress:
+      address: 0.0.0.0
+      portValue: 10080
+  defaultFilterChain:
+    filters:
+    - name: envoy.filters.network.http_connection_manager
+      typedConfig:
+        '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
+        commonHttpProtocolOptions:
+          headersWithUnderscoresAction: REJECT_REQUEST
+        http2ProtocolOptions:
+          initialConnectionWindowSize: 1048576
+          initialStreamWindowSize: 65536
+          maxConcurrentStreams: 100
+        httpFilters:
+        - disabled: true
+          name: envoy.filters.http.ext_authz/securitypolicy/default/policy-for-gateway-1
+          typedConfig:
+            '@type': type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthz
+            failureModeAllow: true
+            httpService:
+              authorizationResponse:
+                allowedUpstreamHeaders:
+                  patterns:
+                  - exact: header1
+                    ignoreCase: true
+                  - exact: header2
+                    ignoreCase: true
+              pathPrefix: /auth
+              retryPolicy:
+                numRetries: 2
+                retryBackOff:
+                  baseInterval: 0.200s
+                  maxInterval: 3s
+                retryOn: 5xx,deadline-exceeded,internal,unavailable
+              serverUri:
+                cluster: securitypolicy/default/policy-for-gateway-1/envoy-gateway/http-backend
+                timeout: 10s
+                uri: http://http-backend.envoy-gateway:80/auth
+            transportApiVersion: V3
+        - disabled: true
+          name: envoy.filters.http.ext_authz/securitypolicy/default/policy-for-http-route-1
+          typedConfig:
+            '@type': type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthz
+            allowedHeaders:
+              patterns:
+              - exact: header1
+                ignoreCase: true
+              - exact: header2
+                ignoreCase: true
+            httpService:
+              retryPolicy:
+                numRetries: 2
+                retryBackOff:
+                  baseInterval: 0.200s
+                  maxInterval: 3s
+                retryOn: 5xx,deadline-exceeded,internal,unavailable
+              serverUri:
+                cluster: securitypolicy/default/policy-for-http-route-1/default/http-backend
+                timeout: 10s
+                uri: http://http-backend.default:9000
+            transportApiVersion: V3
+        - name: envoy.filters.http.router
+          typedConfig:
+            '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
+            suppressEnvoyHeaders: true
+        mergeSlashes: true
+        normalizePath: true
+        pathWithEscapedSlashesAction: UNESCAPE_AND_REDIRECT
+        rds:
+          configSource:
+            ads: {}
+            resourceApiVersion: V3
+          routeConfigName: default/gateway-1/http
+        serverHeaderTransformation: PASS_THROUGH
+        statPrefix: http-10080
+        useRemoteAddress: true
+    name: default/gateway-1/http
+  maxConnectionsToAcceptPerSocketEvent: 1
+  name: default/gateway-1/http
+  perConnectionBufferLimitBytes: 32768
diff --git a/internal/xds/translator/testdata/out/xds-ir/ext-auth-http-retry.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/ext-auth-http-retry.routes.yaml
new file mode 100644
index 000000000..08edfc3c4
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/ext-auth-http-retry.routes.yaml
@@ -0,0 +1,44 @@
+- ignorePortInHostMatching: true
+  name: default/gateway-1/http
+  virtualHosts:
+  - domains:
+    - www.foo.com
+    name: default/gateway-1/http/www_foo_com
+    routes:
+    - match:
+        pathSeparatedPrefix: /foo1
+      name: httproute/default/httproute-1/rule/0/match/0/www_foo_com
+      route:
+        cluster: httproute/default/httproute-1/rule/0
+        upgradeConfigs:
+        - upgradeType: websocket
+      typedPerFilterConfig:
+        envoy.filters.http.ext_authz/securitypolicy/default/policy-for-http-route-1:
+          '@type': type.googleapis.com/envoy.config.route.v3.FilterConfig
+          config: {}
+    - match:
+        pathSeparatedPrefix: /foo2
+      name: httproute/default/httproute-1/rule/1/match/0/www_foo_com
+      route:
+        cluster: httproute/default/httproute-1/rule/1
+        upgradeConfigs:
+        - upgradeType: websocket
+      typedPerFilterConfig:
+        envoy.filters.http.ext_authz/securitypolicy/default/policy-for-http-route-1:
+          '@type': type.googleapis.com/envoy.config.route.v3.FilterConfig
+          config: {}
+  - domains:
+    - www.bar.com
+    name: default/gateway-1/http/www_bar_com
+    routes:
+    - match:
+        pathSeparatedPrefix: /bar
+      name: httproute/default/httproute-2/rule/0/match/0/www_bar_com
+      route:
+        cluster: httproute/default/httproute-2/rule/0
+        upgradeConfigs:
+        - upgradeType: websocket
+      typedPerFilterConfig:
+        envoy.filters.http.ext_authz/securitypolicy/default/policy-for-gateway-1:
+          '@type': type.googleapis.com/envoy.config.route.v3.FilterConfig
+          config: {}
diff --git a/internal/xds/translator/testdata/out/xds-ir/ext-auth-retry.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/ext-auth-retry.listeners.yaml
index cd1dfed29..ad501f621 100644
--- a/internal/xds/translator/testdata/out/xds-ir/ext-auth-retry.listeners.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/ext-auth-retry.listeners.yaml
@@ -28,6 +28,12 @@
                   - exact: header2
                     ignoreCase: true
               pathPrefix: /auth
+              retryPolicy:
+                numRetries: 2
+                retryBackOff:
+                  baseInterval: 0.200s
+                  maxInterval: 3s
+                retryOn: 5xx,deadline-exceeded,internal,unavailable
               serverUri:
                 cluster: securitypolicy/default/policy-for-gateway-1/envoy-gateway/http-backend
                 timeout: 10s
diff --git a/internal/xds/translator/translator_test.go b/internal/xds/translator/translator_test.go
index a327119cd..0f1849377 100644
--- a/internal/xds/translator/translator_test.go
+++ b/internal/xds/translator/translator_test.go
@@ -7,6 +7,7 @@ package translator
 
 import (
 	"embed"
+	"encoding/json"
 	"os"
 	"path/filepath"
 	"runtime"
@@ -15,6 +16,7 @@ import (
 	"testing"
 	"time"
 
+	routev3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
 	"github.com/envoyproxy/go-control-plane/pkg/cache/types"
 	resourcev3 "github.com/envoyproxy/go-control-plane/pkg/resource/v3"
 	ratelimitv3 "github.com/envoyproxy/go-control-plane/ratelimit/config/ratelimit/v3"
@@ -50,6 +52,12 @@ type testFileConfig struct {
 	runtimeFlags              *egv1a1.RuntimeFlags
 }
 
+type rateLimitOutput struct {
+	RouteName     string               `json:"routeName" yaml:"routeName"`
+	RateLimits    []*routev3.RateLimit `json:"rateLimits" yaml:"rateLimits"`
+	CostSpecified bool                 `json:"costSpecified" yaml:"costSpecified"`
+}
+
 func TestTranslateXds(t *testing.T) {
 	// this is a hack to make sure EG render same output on macos and linux
 	defaultCertificateName = "/etc/ssl/certs/ca-certificates.crt"
@@ -247,6 +255,39 @@ func TestTranslateRateLimitConfig(t *testing.T) {
 	}
 }
 
+func TestBuildRouteRateLimits(t *testing.T) {
+	inputFiles, err := filepath.Glob(filepath.Join("testdata", "in", "ratelimit-config", "*.yaml"))
+	require.NoError(t, err)
+
+	for _, inputFile := range inputFiles {
+		inputFileName := testName(inputFile)
+		t.Run(inputFileName, func(t *testing.T) {
+			// Get listeners from the test data
+			listeners := requireXdsIRListenersFromInputTestData(t, inputFile)
+
+			var outputs []rateLimitOutput
+
+			// Process each route to get rate limit actions
+			for _, listener := range listeners {
+				for _, route := range listener.Routes {
+					rateLimits, costSpecified := buildRouteRateLimits(route)
+
+					output := rateLimitOutput{
+						RouteName:     route.Name,
+						RateLimits:    rateLimits,
+						CostSpecified: costSpecified,
+					}
+					outputs = append(outputs, output)
+				}
+			}
+			if test.OverrideTestData() {
+				require.NoError(t, file.Write(requireRouteRateLimitsToYAMLString(t, outputs), filepath.Join("testdata", "out", "ratelimit-config", inputFileName+".routes.yaml")))
+			}
+			require.Equal(t, requireTestDataOutFile(t, "ratelimit-config", inputFileName+".routes.yaml"), requireRouteRateLimitsToYAMLString(t, outputs))
+		})
+	}
+}
+
 // Simulate various extension server hooks and ensure that the translator returns the original resources
 // when configured to failOpen
 func TestTranslateXdsWithExtensionErrorsWhenFailOpen(t *testing.T) {
@@ -567,6 +608,24 @@ func requireRateLimitConfigsToYAMLString(t *testing.T, configs []*ratelimitv3.Ra
 	return result
 }
 
+func requireRouteRateLimitsToYAMLString(t *testing.T, rlOutputs []rateLimitOutput) string {
+	if len(rlOutputs) == 0 {
+		return ""
+	}
+
+	results := make([]string, 0, len(rlOutputs))
+	for _, output := range rlOutputs {
+		jsonBytes, err := json.Marshal(output)
+		require.NoError(t, err)
+		yamlBytes, err := yaml.JSONToYAML(jsonBytes)
+		require.NoError(t, err)
+
+		results = append(results, string(yamlBytes))
+	}
+
+	return strings.Join(results, "---\n")
+}
+
 func requireResourcesToYAMLString(t *testing.T, resources []types.Resource) string {
 	jsonBytes, err := utils.MarshalResourcesToJSON(resources)
 	require.NoError(t, err)
diff --git a/site/content/en/latest/api/extension_types.md b/site/content/en/latest/api/extension_types.md
index da94c842a..1592fe841 100644
--- a/site/content/en/latest/api/extension_types.md
+++ b/site/content/en/latest/api/extension_types.md
@@ -1391,6 +1391,7 @@ _Appears in:_
 | ---   | ---  | ---      | ---     | ---         |
 | `rateLimitDeployment` | _[KubernetesDeploymentSpec](#kubernetesdeploymentspec)_ |  false  |  | RateLimitDeployment defines the desired state of the Envoy ratelimit deployment resource.<br />If unspecified, default settings for the managed Envoy ratelimit deployment resource<br />are applied. |
 | `rateLimitHpa` | _[KubernetesHorizontalPodAutoscalerSpec](#kuberneteshorizontalpodautoscalerspec)_ |  false  |  | RateLimitHpa defines the Horizontal Pod Autoscaler settings for Envoy ratelimit Deployment.<br />If the HPA is set, Replicas field from RateLimitDeployment will be ignored. |
+| `rateLimitPDB` | _[KubernetesPodDisruptionBudgetSpec](#kubernetespoddisruptionbudgetspec)_ |  false  |  | RateLimitPDB allows to control the pod disruption budget of rate limit service. |
 | `watch` | _[KubernetesWatchMode](#kuberneteswatchmode)_ |  false  |  | Watch holds configuration of which input resources should be watched and reconciled. |
 | `leaderElection` | _[LeaderElection](#leaderelection)_ |  false  |  | LeaderElection specifies the configuration for leader election.<br />If it's not set up, leader election will be active by default, using Kubernetes' standard settings. |
 | `shutdownManager` | _[ShutdownManager](#shutdownmanager)_ |  false  |  | ShutdownManager defines the configuration for the shutdown manager. |
@@ -2585,8 +2586,8 @@ _Appears in:_
 | `disableRateLimitHeaders` | _boolean_ |  false  |  | DisableRateLimitHeaders configures Envoy Proxy to omit the "X-RateLimit-" response headers<br />when rate limiting is enabled. |
 | `xForwardedClientCert` | _[XForwardedClientCert](#xforwardedclientcert)_ |  false  |  | XForwardedClientCert configures how Envoy Proxy handle the x-forwarded-client-cert (XFCC) HTTP header.<br />x-forwarded-client-cert (XFCC) is an HTTP header used to forward the certificate<br />information of part or all of the clients or proxies that a request has flowed through,<br />on its way from the client to the server.<br />Envoy proxy may choose to sanitize/append/forward the XFCC header before proxying the request.<br />If not set, the default behavior is sanitizing the XFCC header. |
 | `withUnderscoresAction` | _[WithUnderscoresAction](#withunderscoresaction)_ |  false  |  | WithUnderscoresAction configures the action to take when an HTTP header with underscores<br />is encountered. The default action is to reject the request. |
-| `preserveXRequestID` | _boolean_ |  false  |  | PreserveXRequestID configures Envoy to keep the X-Request-ID header if passed for a request that is edge<br />(Edge request is the request from external clients to front Envoy) and not reset it, which is the current Envoy behaviour.<br />Defaults to false and cannot be combined with RequestID.<br />Deprecated: use RequestID=Preserve instead |
-| `requestID` | _[RequestIDAction](#requestidaction)_ |  false  |  | RequestID configures Envoy's behavior for handling the `X-Request-ID` header.<br />Defaults to `Generate` and builds the `X-Request-ID` for every request and ignores pre-existing values from the edge.<br />(An "edge request" refers to a request from an external client to the Envoy entrypoint.) |
+| `preserveXRequestID` | _boolean_ |  false  |  | PreserveXRequestID configures Envoy to keep the X-Request-ID header if passed for a request that is edge<br />(Edge request is the request from external clients to front Envoy) and not reset it, which is the current Envoy behaviour.<br />Defaults to false and cannot be combined with RequestID.<br />Deprecated: use RequestID=PreserveOrGenerate instead |
+| `requestID` | _[RequestIDAction](#requestidaction)_ |  false  |  | RequestID configures Envoy's behavior for handling the `X-Request-ID` header.<br />When omitted default behavior is `Generate` which builds the `X-Request-ID` for every request<br /> and ignores pre-existing values from the edge.<br />(An "edge request" refers to a request from an external client to the Envoy entrypoint.) |
 | `earlyRequestHeaders` | _[HTTPHeaderFilter](#httpheaderfilter)_ |  false  |  | EarlyRequestHeaders defines settings for early request header modification, before envoy performs<br />routing, tracing and built-in header manipulation. |
 | `lateResponseHeaders` | _[HTTPHeaderFilter](#httpheaderfilter)_ |  false  |  | LateResponseHeaders defines settings for global response header modification. |
 
@@ -3059,6 +3060,7 @@ _Appears in:_
 KubernetesPodDisruptionBudgetSpec defines Kubernetes PodDisruptionBudget settings of Envoy Proxy Deployment.
 
 _Appears in:_
+- [EnvoyGatewayKubernetesProvider](#envoygatewaykubernetesprovider)
 - [EnvoyProxyKubernetesProvider](#envoyproxykubernetesprovider)
 
 | Field | Type | Required | Default | Description |
@@ -3365,6 +3367,21 @@ _Appears in:_
 | `JSONMerge` | JSONMerge indicates a JSON merge patch type<br /> | 
 
 
+#### MethodMatch
+
+
+
+MethodMatch defines the matching criteria for the HTTP method of a request.
+
+_Appears in:_
+- [RateLimitSelectCondition](#ratelimitselectcondition)
+
+| Field | Type | Required | Default | Description |
+| ---   | ---  | ---      | ---     | ---         |
+| `value` | _[HTTPMethod](#httpmethod)_ |  true  |  | Value specifies the HTTP method. |
+| `invert` | _boolean_ |  false  | false | Invert specifies whether the value match result will be inverted. |
+
+
 #### MetricSinkType
 
 _Underlying type:_ _string_
@@ -3521,7 +3538,7 @@ _Appears in:_
 
 | Field | Type | Required | Default | Description |
 | ---   | ---  | ---      | ---     | ---         |
-| `methods` | _HTTPMethod array_ |  true  |  | Methods are the HTTP methods of the request.<br />If multiple methods are specified, all specified methods are allowed or denied, based on the action of the rule. |
+| `methods` | _[HTTPMethod](#httpmethod) array_ |  true  |  | Methods are the HTTP methods of the request.<br />If multiple methods are specified, all specified methods are allowed or denied, based on the action of the rule. |
 
 
 #### Origin
@@ -3603,6 +3620,22 @@ _Appears in:_
 | `UnescapeAndForward` | UnescapeAndForward unescapes %2F and %5C sequences and forwards the request.<br />Note: this option should not be enabled if intermediaries perform path based access<br />control as it may lead to path confusion vulnerabilities.<br /> | 
 
 
+#### PathMatch
+
+
+
+PathMatch defines the matching criteria for the HTTP path of a request.
+
+_Appears in:_
+- [RateLimitSelectCondition](#ratelimitselectcondition)
+
+| Field | Type | Required | Default | Description |
+| ---   | ---  | ---      | ---     | ---         |
+| `type` | _[PathMatchType](#pathmatchtype)_ |  false  | PathPrefix | Type specifies how to match against the value of the path. |
+| `value` | _string_ |  true  | / | Value specifies the HTTP path. |
+| `invert` | _boolean_ |  false  | false | Invert specifies whether the value match result will be inverted. |
+
+
 #### PathSettings
 
 
@@ -4259,14 +4292,17 @@ _Appears in:_
 RateLimitSelectCondition specifies the attributes within the traffic flow that can
 be used to select a subset of clients to be ratelimited.
 All the individual conditions must hold True for the overall condition to hold True.
+And, at least one of headers or methods or path or sourceCIDR condition must be specified.
 
 _Appears in:_
 - [RateLimitRule](#ratelimitrule)
 
 | Field | Type | Required | Default | Description |
 | ---   | ---  | ---      | ---     | ---         |
-| `headers` | _[HeaderMatch](#headermatch) array_ |  false  |  | Headers is a list of request headers to match. Multiple header values are ANDed together,<br />meaning, a request MUST match all the specified headers.<br />At least one of headers or sourceCIDR condition must be specified. |
-| `sourceCIDR` | _[SourceMatch](#sourcematch)_ |  false  |  | SourceCIDR is the client IP Address range to match on.<br />At least one of headers or sourceCIDR condition must be specified. |
+| `headers` | _[HeaderMatch](#headermatch) array_ |  false  |  | Headers is a list of request headers to match. Multiple header values are ANDed together,<br />meaning, a request MUST match all the specified headers. |
+| `methods` | _[MethodMatch](#methodmatch) array_ |  false  |  | Methods is a list of request methods to match. Multiple method values are ORed together,<br />meaning, a request can match any one of the specified methods. If not specified, it matches all methods. |
+| `path` | _[PathMatch](#pathmatch)_ |  false  |  | Path is the request path to match.<br />Support Exact, PathPrefix and RegularExpression match types. |
+| `sourceCIDR` | _[SourceMatch](#sourcematch)_ |  false  |  | SourceCIDR is the client IP Address range to match on. |
 
 
 #### RateLimitSpec
@@ -4471,7 +4507,8 @@ _Appears in:_
 
 _Underlying type:_ _string_
 
-RequestIDAction configures Envoy's behavior for handling the `X-Request-ID` header.
+RequestIDAction configures Envoy's behavior for handling the `X-Request-ID` header at the edge.
+An "edge request" refers to a request from an external client to the Envoy entrypoint.
 
 _Appears in:_
 - [HeaderSettings](#headersettings)
diff --git a/site/content/en/latest/concepts/gateway_api_extensions/backend-traffic-policy.md b/site/content/en/latest/concepts/gateway_api_extensions/backend-traffic-policy.md
index 06c99d39e..a395d3b8d 100644
--- a/site/content/en/latest/concepts/gateway_api_extensions/backend-traffic-policy.md
+++ b/site/content/en/latest/concepts/gateway_api_extensions/backend-traffic-policy.md
@@ -74,10 +74,12 @@ The policy applies to all resources that match either targeting method. You can
 
 ### Precedence
 
-When multiple BackendTrafficPolicies apply to the same resource, Envoy Gateway resolves conflicts using a precedence hierarchy based on the target resource type, regardless of how the policy was attached:
+When multiple BackendTrafficPolicies apply to the same resource, Envoy Gateway resolves conflicts using a precedence hierarchy based on the target resource type and section-level specificity:
 
-1. **Route-level policies** (HTTPRoute, GRPCRoute, etc.) - Highest precedence
-2. **Gateway-level policies** - Lower precedence
+1. **Route rule-level policies** (HTTPRoute/GRPCRoute with `sectionName` targeting specific rules) - Highest precedence
+2. **Route-level policies** (HTTPRoute, GRPCRoute without `sectionName`) - High precedence
+3. **Listener-level policies** (Gateway with `sectionName` targeting specific listeners) - Medium precedence
+4. **Gateway-level policies** (Gateway without `sectionName`) - Lowest precedence
 
 ```yaml
 # Gateway-level policy (lower precedence) - Applies to all routes in the gateway
@@ -150,7 +152,9 @@ When the `mergeType` field is unset, no merging occurs and only the most specifi
 
 ## Policy Merging
 
-BackendTrafficPolicy supports merging configurations using the `mergeType` field, which allows route-level policies to combine with gateway-level policies rather than completely overriding them. This enables layered policy strategies where platform teams can set baseline configurations at the Gateway level, while application teams can add specific policies for their routes.
+BackendTrafficPolicy supports merging configurations using the `mergeType` field, which allows route-level or route rule-level policies to combine with gateway-level or listener-level policies rather than completely overriding them. This enables layered policy strategies where platform teams can set baseline configurations at the Gateway level, while application teams can add specific policies for their routes.
+
+When merging occurs, route-level policies will merge with either a gateway-level or listener-level policy, but not both. If both gateway and listener policies exist, the listener-level policy takes precedence.
 
 ### Merge Types
 
diff --git a/site/content/en/latest/tasks/traffic/global-rate-limit.md b/site/content/en/latest/tasks/traffic/global-rate-limit.md
index ee1a4950b..e4a295459 100644
--- a/site/content/en/latest/tasks/traffic/global-rate-limit.md
+++ b/site/content/en/latest/tasks/traffic/global-rate-limit.md
@@ -954,6 +954,373 @@ transfer-encoding: chunked
 
 ```
 
+## Rate Limit Based on Path
+
+Here is an example of a rate limit implemented by the application developer to limit requests based on the request path. In this case, we are limiting requests to `/api/users` to 3 requests/Hour.
+
+{{< tabpane text=true >}}
+{{% tab header="Apply from stdin" %}}
+
+```shell
+cat <<EOF | kubectl apply -f -
+apiVersion: gateway.envoyproxy.io/v1alpha1
+kind: BackendTrafficPolicy 
+metadata:
+  name: policy-httproute
+spec:
+  targetRefs:
+  - group: gateway.networking.k8s.io
+    kind: HTTPRoute
+    name: http-ratelimit
+  rateLimit:
+    type: Global
+    global:
+      rules:
+      - clientSelectors:
+        - path:
+            type: Exact
+            value: /api/users
+        limit:
+          requests: 3
+          unit: Hour
+---
+apiVersion: gateway.networking.k8s.io/v1
+kind: HTTPRoute
+metadata:
+  name: http-ratelimit
+spec:
+  parentRefs:
+  - name: eg
+  hostnames:
+  - ratelimit.example 
+  rules:
+  - matches:
+    - path:
+        type: PathPrefix
+        value: /api
+    backendRefs:
+    - group: ""
+      kind: Service
+      name: backend
+      port: 3000
+EOF
+```
+
+{{% /tab %}}
+{{% tab header="Apply from file" %}}
+Save and apply the following resource to your cluster:
+
+```yaml
+---
+apiVersion: gateway.envoyproxy.io/v1alpha1
+kind: BackendTrafficPolicy 
+metadata:
+  name: policy-httproute
+spec:
+  targetRefs:
+  - group: gateway.networking.k8s.io
+    kind: HTTPRoute
+    name: http-ratelimit
+  rateLimit:
+    type: Global
+    global:
+      rules:
+      - clientSelectors:
+        - path:
+            type: Exact
+            value: /api/users
+        limit:
+          requests: 3
+          unit: Hour
+---
+apiVersion: gateway.networking.k8s.io/v1
+kind: HTTPRoute
+metadata:
+  name: http-ratelimit
+spec:
+  parentRefs:
+  - name: eg
+  hostnames:
+  - ratelimit.example 
+  rules:
+  - matches:
+    - path:
+        type: PathPrefix
+        value: /api
+    backendRefs:
+    - group: ""
+      kind: Service
+      name: backend
+      port: 3000
+```
+
+{{% /tab %}}
+{{< /tabpane >}}
+
+The path matching supports three types:
+- `Exact`: Matches the exact path (e.g., `/api/users`)
+- `PathPrefix`: Matches paths with a specific prefix (e.g., `/api/`)
+- `RegularExpression`: Matches paths using regex patterns (e.g., `/api/users/[0-9]+`)
+
+Let's query `/api/users` 4 times and `/api/products` 4 times. We should receive a `200` response for the first 3 requests to `/api/users`
+and a `429` status code for the 4th request. Requests to `/api/products` should not be rate limited:
+
+```shell
+# First, test the rate-limited path /api/users - 4 requests (4th should be rate limited)
+for i in {1..4}; do curl -I --header "Host: ratelimit.example" http://${GATEWAY_HOST}/api/users ; sleep 1; done
+
+# Now test a different path that should not be rate limited
+for i in {1..4}; do curl -I --header "Host: ratelimit.example" http://${GATEWAY_HOST}/api/products ; sleep 1; done
+```
+
+```console
+# Requests to /api/users (rate limited after 3 requests)
+HTTP/1.1 200 OK
+content-type: application/json
+x-content-type-options: nosniff
+date: Wed, 08 Feb 2023 02:33:31 GMT
+content-length: 460
+x-envoy-upstream-service-time: 4
+server: envoy
+
+HTTP/1.1 200 OK
+content-type: application/json
+x-content-type-options: nosniff
+date: Wed, 08 Feb 2023 02:33:32 GMT
+content-length: 460
+x-envoy-upstream-service-time: 2
+server: envoy
+
+HTTP/1.1 200 OK
+content-type: application/json
+x-content-type-options: nosniff
+date: Wed, 08 Feb 2023 02:33:33 GMT
+content-length: 460
+x-envoy-upstream-service-time: 0
+server: envoy
+
+HTTP/1.1 429 Too Many Requests
+x-envoy-ratelimited: true
+date: Wed, 08 Feb 2023 02:33:34 GMT
+server: envoy
+transfer-encoding: chunked
+
+# Requests to /api/products (not rate limited)
+HTTP/1.1 200 OK
+content-type: application/json
+x-content-type-options: nosniff
+date: Wed, 08 Feb 2023 02:33:35 GMT
+content-length: 460
+x-envoy-upstream-service-time: 4
+server: envoy
+
+HTTP/1.1 200 OK
+content-type: application/json
+x-content-type-options: nosniff
+date: Wed, 08 Feb 2023 02:33:36 GMT
+content-length: 460
+x-envoy-upstream-service-time: 2
+server: envoy
+
+HTTP/1.1 200 OK
+content-type: application/json
+x-content-type-options: nosniff
+date: Wed, 08 Feb 2023 02:33:37 GMT
+content-length: 460
+x-envoy-upstream-service-time: 0
+server: envoy
+
+HTTP/1.1 200 OK
+content-type: application/json
+x-content-type-options: nosniff
+date: Wed, 08 Feb 2023 02:33:38 GMT
+content-length: 460
+x-envoy-upstream-service-time: 0
+server: envoy
+```
+
+You can see that requests to `/api/users` are rate limited after the 3rd request (returning 429), while all requests to `/api/products` succeed without any rate limit.
+
+## Rate Limit Based on HTTP Method
+
+Here is an example of a rate limit implemented by the application developer to limit requests based on the HTTP method. In this case, we are limiting POST requests to 3 requests/Hour.
+
+{{< tabpane text=true >}}
+{{% tab header="Apply from stdin" %}}
+
+```shell
+cat <<EOF | kubectl apply -f -
+apiVersion: gateway.envoyproxy.io/v1alpha1
+kind: BackendTrafficPolicy 
+metadata:
+  name: policy-httproute
+spec:
+  targetRefs:
+  - group: gateway.networking.k8s.io
+    kind: HTTPRoute
+    name: http-ratelimit
+  rateLimit:
+    type: Global
+    global:
+      rules:
+      - clientSelectors:
+        - methods:
+          - value: POST
+        limit:
+          requests: 3
+          unit: Hour
+---
+apiVersion: gateway.networking.k8s.io/v1
+kind: HTTPRoute
+metadata:
+  name: http-ratelimit
+spec:
+  parentRefs:
+  - name: eg
+  hostnames:
+  - ratelimit.example 
+  rules:
+  - matches:
+    - path:
+        type: PathPrefix
+        value: /
+    backendRefs:
+    - group: ""
+      kind: Service
+      name: backend
+      port: 3000
+EOF
+```
+
+{{% /tab %}}
+{{% tab header="Apply from file" %}}
+Save and apply the following resource to your cluster:
+
+```yaml
+---
+apiVersion: gateway.envoyproxy.io/v1alpha1
+kind: BackendTrafficPolicy 
+metadata:
+  name: policy-httproute
+spec:
+  targetRefs:
+  - group: gateway.networking.k8s.io
+    kind: HTTPRoute
+    name: http-ratelimit
+  rateLimit:
+    type: Global
+    global:
+      rules:
+      - clientSelectors:
+        - methods:
+          - value: POST
+        limit:
+          requests: 3
+          unit: Hour
+---
+apiVersion: gateway.networking.k8s.io/v1
+kind: HTTPRoute
+metadata:
+  name: http-ratelimit
+spec:
+  parentRefs:
+  - name: eg
+  hostnames:
+  - ratelimit.example 
+  rules:
+  - matches:
+    - path:
+        type: PathPrefix
+        value: /
+    backendRefs:
+    - group: ""
+      kind: Service
+      name: backend
+      port: 3000
+```
+
+{{% /tab %}}
+{{< /tabpane >}}
+
+Let's query the same path with POST and GET methods. We should receive a `200` response for the first 3 POST requests
+and a `429` status code for the 4th POST request. GET requests should not be rate limited:
+
+```shell
+# Send 4 POST requests - the 4th one should be rate limited
+for i in {1..4}; do curl -X POST -I --header "Host: ratelimit.example" http://${GATEWAY_HOST}/api/data ; sleep 1; done
+
+# GET requests should not be rate limited
+for i in {1..4}; do curl -X GET -I --header "Host: ratelimit.example" http://${GATEWAY_HOST}/api/data ; sleep 1; done
+```
+
+```console
+# POST requests (rate limited after 3 requests)
+HTTP/1.1 200 OK
+content-type: application/json
+x-content-type-options: nosniff
+date: Wed, 08 Feb 2023 02:33:31 GMT
+content-length: 460
+x-envoy-upstream-service-time: 4
+server: envoy
+
+HTTP/1.1 200 OK
+content-type: application/json
+x-content-type-options: nosniff
+date: Wed, 08 Feb 2023 02:33:32 GMT
+content-length: 460
+x-envoy-upstream-service-time: 2
+server: envoy
+
+HTTP/1.1 200 OK
+content-type: application/json
+x-content-type-options: nosniff
+date: Wed, 08 Feb 2023 02:33:33 GMT
+content-length: 460
+x-envoy-upstream-service-time: 0
+server: envoy
+
+HTTP/1.1 429 Too Many Requests
+x-envoy-ratelimited: true
+date: Wed, 08 Feb 2023 02:33:34 GMT
+server: envoy
+transfer-encoding: chunked
+
+# GET requests (not rate limited)
+HTTP/1.1 200 OK
+content-type: application/json
+x-content-type-options: nosniff
+date: Wed, 08 Feb 2023 02:33:35 GMT
+content-length: 460
+x-envoy-upstream-service-time: 4
+server: envoy
+
+HTTP/1.1 200 OK
+content-type: application/json
+x-content-type-options: nosniff
+date: Wed, 08 Feb 2023 02:33:36 GMT
+content-length: 460
+x-envoy-upstream-service-time: 2
+server: envoy
+
+HTTP/1.1 200 OK
+content-type: application/json
+x-content-type-options: nosniff
+date: Wed, 08 Feb 2023 02:33:37 GMT
+content-length: 460
+x-envoy-upstream-service-time: 0
+server: envoy
+
+HTTP/1.1 200 OK
+content-type: application/json
+x-content-type-options: nosniff
+date: Wed, 08 Feb 2023 02:33:38 GMT
+content-length: 460
+x-envoy-upstream-service-time: 0
+server: envoy
+```
+
+You can see that POST requests are rate limited after the 3rd request (returning 429), while GET requests to the same path are not rate limited.
+
 ## Rate Limit Jwt Claims
 
 Here is an example of a rate limit implemented by the application developer to limit distinct users who can be differentiated based on the value of the Jwt claims carried.
diff --git a/site/content/en/latest/tasks/traffic/local-rate-limit.md b/site/content/en/latest/tasks/traffic/local-rate-limit.md
index 05caf0725..c5492b823 100644
--- a/site/content/en/latest/tasks/traffic/local-rate-limit.md
+++ b/site/content/en/latest/tasks/traffic/local-rate-limit.md
@@ -620,6 +620,409 @@ transfer-encoding: chunked
 **Note:** Local rate limiting does not support `distinct` matching. If you want to rate limit based on distinct values, 
 you should use [Global Rate Limiting][]. 
 
+## Rate Limit Based on Path
+
+This example shows you how to rate limit requests based on the request path. In this case, we want to limit requests to `/api/users` to 3 requests/Hour.
+
+{{< tabpane text=true >}}
+{{% tab header="Apply from stdin" %}}
+
+```shell
+cat <<EOF | kubectl apply -f -
+apiVersion: gateway.envoyproxy.io/v1alpha1
+kind: BackendTrafficPolicy 
+metadata:
+  name: policy-httproute
+spec:
+  targetRefs:
+  - group: gateway.networking.k8s.io
+    kind: HTTPRoute
+    name: http-ratelimit
+  rateLimit:
+    type: Local
+    local:
+      rules:
+      - clientSelectors:
+        - path:
+            type: Exact
+            value: /api/users
+        limit:
+          requests: 3
+          unit: Hour
+EOF
+```
+
+{{% /tab %}}
+{{% tab header="Apply from file" %}}
+Save and apply the following resource to your cluster:
+
+```yaml
+---
+apiVersion: gateway.envoyproxy.io/v1alpha1
+kind: BackendTrafficPolicy 
+metadata:
+  name: policy-httproute
+spec:
+  targetRefs:
+  - group: gateway.networking.k8s.io
+    kind: HTTPRoute
+    name: http-ratelimit
+  rateLimit:
+    type: Local
+    local:
+      rules:
+      - clientSelectors:
+        - path:
+            type: Exact
+            value: /api/users
+        limit:
+          requests: 3
+          unit: Hour
+```
+
+{{% /tab %}}
+{{< /tabpane >}}
+
+The path matching supports three types:
+- `Exact`: Matches the exact path (e.g., `/api/users`)
+- `PathPrefix`: Matches paths with a specific prefix (e.g., `/api/`)
+- `RegularExpression`: Matches paths using regex patterns (e.g., `/api/users/[0-9]+`)
+
+### HTTPRoute
+
+{{< tabpane text=true >}}
+{{% tab header="Apply from stdin" %}}
+
+```shell
+cat <<EOF | kubectl apply -f -
+apiVersion: gateway.networking.k8s.io/v1
+kind: HTTPRoute
+metadata:
+  name: http-ratelimit
+spec:
+  parentRefs:
+  - name: eg
+  hostnames:
+  - ratelimit.example 
+  rules:
+  - matches:
+    - path:
+        type: PathPrefix
+        value: /api
+    backendRefs:
+    - group: ""
+      kind: Service
+      name: backend
+      port: 3000
+EOF
+```
+
+{{% /tab %}}
+{{% tab header="Apply from file" %}}
+Save and apply the following resource to your cluster:
+
+```yaml
+---
+apiVersion: gateway.networking.k8s.io/v1
+kind: HTTPRoute
+metadata:
+  name: http-ratelimit
+spec:
+  parentRefs:
+  - name: eg
+  hostnames:
+  - ratelimit.example 
+  rules:
+  - matches:
+    - path:
+        type: PathPrefix
+        value: /api
+    backendRefs:
+    - group: ""
+      kind: Service
+      name: backend
+      port: 3000
+```
+
+{{% /tab %}}
+{{< /tabpane >}}
+
+Let's test the path-based rate limit by querying `/api/users` and `/api/products`. The rate limit of 3 requests/Hour is only applied to `/api/users`:
+
+```shell
+# First, test the rate-limited path /api/users - 4 requests (4th should be rate limited)
+for i in {1..4}; do curl -I --header "Host: ratelimit.example" http://${GATEWAY_HOST}/api/users ; sleep 1; done
+
+# Now test a different path that should not be rate limited
+for i in {1..4}; do curl -I --header "Host: ratelimit.example" http://${GATEWAY_HOST}/api/products ; sleep 1; done
+```
+
+```console
+# Requests to /api/users (rate limited after 3 requests)
+HTTP/1.1 200 OK
+content-type: application/json
+x-content-type-options: nosniff
+date: Wed, 08 Feb 2023 02:33:31 GMT
+content-length: 460
+x-envoy-upstream-service-time: 4
+server: envoy
+
+HTTP/1.1 200 OK
+content-type: application/json
+x-content-type-options: nosniff
+date: Wed, 08 Feb 2023 02:33:32 GMT
+content-length: 460
+x-envoy-upstream-service-time: 2
+server: envoy
+
+HTTP/1.1 200 OK
+content-type: application/json
+x-content-type-options: nosniff
+date: Wed, 08 Feb 2023 02:33:33 GMT
+content-length: 460
+x-envoy-upstream-service-time: 0
+server: envoy
+
+HTTP/1.1 429 Too Many Requests
+x-envoy-ratelimited: true
+date: Wed, 08 Feb 2023 02:33:34 GMT
+server: envoy
+transfer-encoding: chunked
+
+# Requests to /api/products (not rate limited)
+HTTP/1.1 200 OK
+content-type: application/json
+x-content-type-options: nosniff
+date: Wed, 08 Feb 2023 02:33:35 GMT
+content-length: 460
+x-envoy-upstream-service-time: 4
+server: envoy
+
+HTTP/1.1 200 OK
+content-type: application/json
+x-content-type-options: nosniff
+date: Wed, 08 Feb 2023 02:33:36 GMT
+content-length: 460
+x-envoy-upstream-service-time: 2
+server: envoy
+
+HTTP/1.1 200 OK
+content-type: application/json
+x-content-type-options: nosniff
+date: Wed, 08 Feb 2023 02:33:37 GMT
+content-length: 460
+x-envoy-upstream-service-time: 0
+server: envoy
+
+HTTP/1.1 200 OK
+content-type: application/json
+x-content-type-options: nosniff
+date: Wed, 08 Feb 2023 02:33:38 GMT
+content-length: 460
+x-envoy-upstream-service-time: 0
+server: envoy
+```
+
+As you can see, requests to `/api/users` are rate limited after the 3rd request (returning 429), while all requests to `/api/products` succeed without any rate limit.
+
+## Rate Limit Based on HTTP Method
+
+This example shows you how to rate limit requests based on the HTTP method. In this case, we want to limit POST requests to 3 requests/Hour.
+
+{{< tabpane text=true >}}
+{{% tab header="Apply from stdin" %}}
+
+```shell
+cat <<EOF | kubectl apply -f -
+apiVersion: gateway.envoyproxy.io/v1alpha1
+kind: BackendTrafficPolicy 
+metadata:
+  name: policy-httproute
+spec:
+  targetRefs:
+  - group: gateway.networking.k8s.io
+    kind: HTTPRoute
+    name: http-ratelimit
+  rateLimit:
+    type: Local
+    local:
+      rules:
+      - clientSelectors:
+        - methods:
+          - value: POST
+        limit:
+          requests: 3
+          unit: Hour
+EOF
+```
+
+{{% /tab %}}
+{{% tab header="Apply from file" %}}
+Save and apply the following resource to your cluster:
+
+```yaml
+---
+apiVersion: gateway.envoyproxy.io/v1alpha1
+kind: BackendTrafficPolicy 
+metadata:
+  name: policy-httproute
+spec:
+  targetRefs:
+  - group: gateway.networking.k8s.io
+    kind: HTTPRoute
+    name: http-ratelimit
+  rateLimit:
+    type: Local
+    local:
+      rules:
+      - clientSelectors:
+        - methods:
+          - value: POST
+        limit:
+          requests: 3
+          unit: Hour
+```
+
+{{% /tab %}}
+{{< /tabpane >}}
+
+### HTTPRoute
+
+{{< tabpane text=true >}}
+{{% tab header="Apply from stdin" %}}
+
+```shell
+cat <<EOF | kubectl apply -f -
+apiVersion: gateway.networking.k8s.io/v1
+kind: HTTPRoute
+metadata:
+  name: http-ratelimit
+spec:
+  parentRefs:
+  - name: eg
+  hostnames:
+  - ratelimit.example 
+  rules:
+  - matches:
+    - path:
+        type: PathPrefix
+        value: /
+    backendRefs:
+    - group: ""
+      kind: Service
+      name: backend
+      port: 3000
+EOF
+```
+
+{{% /tab %}}
+{{% tab header="Apply from file" %}}
+Save and apply the following resource to your cluster:
+
+```yaml
+---
+apiVersion: gateway.networking.k8s.io/v1
+kind: HTTPRoute
+metadata:
+  name: http-ratelimit
+spec:
+  parentRefs:
+  - name: eg
+  hostnames:
+  - ratelimit.example 
+  rules:
+  - matches:
+    - path:
+        type: PathPrefix
+        value: /
+    backendRefs:
+    - group: ""
+      kind: Service
+      name: backend
+      port: 3000
+```
+
+{{% /tab %}}
+{{< /tabpane >}}
+
+Let's test the method-based rate limit. POST requests are limited to 3 requests/Hour, while GET requests are not limited:
+
+```shell
+# Send 4 POST requests - the 4th one should be rate limited
+for i in {1..4}; do curl -X POST -I --header "Host: ratelimit.example" http://${GATEWAY_HOST}/api/data ; sleep 1; done
+
+# GET requests should not be rate limited
+for i in {1..4}; do curl -X GET -I --header "Host: ratelimit.example" http://${GATEWAY_HOST}/api/data ; sleep 1; done
+```
+
+```console
+# POST requests (rate limited after 3 requests)
+HTTP/1.1 200 OK
+content-type: application/json
+x-content-type-options: nosniff
+date: Wed, 08 Feb 2023 02:33:31 GMT
+content-length: 460
+x-envoy-upstream-service-time: 4
+server: envoy
+
+HTTP/1.1 200 OK
+content-type: application/json
+x-content-type-options: nosniff
+date: Wed, 08 Feb 2023 02:33:32 GMT
+content-length: 460
+x-envoy-upstream-service-time: 2
+server: envoy
+
+HTTP/1.1 200 OK
+content-type: application/json
+x-content-type-options: nosniff
+date: Wed, 08 Feb 2023 02:33:33 GMT
+content-length: 460
+x-envoy-upstream-service-time: 0
+server: envoy
+
+HTTP/1.1 429 Too Many Requests
+x-envoy-ratelimited: true
+date: Wed, 08 Feb 2023 02:33:34 GMT
+server: envoy
+transfer-encoding: chunked
+
+# GET requests (not rate limited)
+HTTP/1.1 200 OK
+content-type: application/json
+x-content-type-options: nosniff
+date: Wed, 08 Feb 2023 02:33:35 GMT
+content-length: 460
+x-envoy-upstream-service-time: 4
+server: envoy
+
+HTTP/1.1 200 OK
+content-type: application/json
+x-content-type-options: nosniff
+date: Wed, 08 Feb 2023 02:33:36 GMT
+content-length: 460
+x-envoy-upstream-service-time: 2
+server: envoy
+
+HTTP/1.1 200 OK
+content-type: application/json
+x-content-type-options: nosniff
+date: Wed, 08 Feb 2023 02:33:37 GMT
+content-length: 460
+x-envoy-upstream-service-time: 0
+server: envoy
+
+HTTP/1.1 200 OK
+content-type: application/json
+x-content-type-options: nosniff
+date: Wed, 08 Feb 2023 02:33:38 GMT
+content-length: 460
+x-envoy-upstream-service-time: 0
+server: envoy
+```
+
+As you can see, POST requests are rate limited after the 3rd request (returning 429), while GET requests to the same path are not rate limited.
+
 [Global Rate Limiting]: https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/other_features/global_rate_limiting
 [Local rate limiting]: https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/other_features/local_rate_limiting
 [BackendTrafficPolicy]: ../../../api/extension_types#backendtrafficpolicy
diff --git a/test/cel-validation/backendtrafficpolicy_test.go b/test/cel-validation/backendtrafficpolicy_test.go
index a6c5a0b4f..213c00620 100644
--- a/test/cel-validation/backendtrafficpolicy_test.go
+++ b/test/cel-validation/backendtrafficpolicy_test.go
@@ -164,7 +164,7 @@ func TestBackendTrafficPolicyTarget(t *testing.T) {
 			},
 		},
 		{
-			desc: "sectionName disabled until supported",
+			desc: "sectionName supported",
 			mutate: func(btp *egv1a1.BackendTrafficPolicy) {
 				btp.Spec = egv1a1.BackendTrafficPolicySpec{
 					PolicyTargetReferences: egv1a1.PolicyTargetReferences{
@@ -179,9 +179,7 @@ func TestBackendTrafficPolicyTarget(t *testing.T) {
 					},
 				}
 			},
-			wantErrors: []string{
-				"spec: Invalid value: \"object\": this policy does not yet support the sectionName field",
-			},
+			wantErrors: []string{},
 		},
 		{
 			desc: "consistentHash field not nil when type is consistentHash",
@@ -2008,6 +2006,38 @@ func TestBackendTrafficPolicyTarget(t *testing.T) {
 			},
 			wantErrors: []string{`response cost is not supported for Local Rate Limits`},
 		},
+		{
+			desc: "invalid RateLimitSelectCondition with no selectors",
+			mutate: func(btp *egv1a1.BackendTrafficPolicy) {
+				btp.Spec = egv1a1.BackendTrafficPolicySpec{
+					PolicyTargetReferences: egv1a1.PolicyTargetReferences{
+						TargetRef: &gwapiv1.LocalPolicyTargetReferenceWithSectionName{
+							LocalPolicyTargetReference: gwapiv1.LocalPolicyTargetReference{
+								Group: gwapiv1.Group("gateway.networking.k8s.io"),
+								Kind:  gwapiv1.Kind("Gateway"),
+								Name:  gwapiv1.ObjectName("eg"),
+							},
+						},
+					},
+					RateLimit: &egv1a1.RateLimitSpec{
+						Type: egv1a1.GlobalRateLimitType,
+						Global: &egv1a1.GlobalRateLimit{
+							Rules: []egv1a1.RateLimitRule{
+								{
+									ClientSelectors: []egv1a1.RateLimitSelectCondition{
+										{
+											// No headers, methods, path, or sourceCIDR specified
+										},
+									},
+									Limit: egv1a1.RateLimitValue{Requests: 10, Unit: "Minute"},
+								},
+							},
+						},
+					},
+				}
+			},
+			wantErrors: []string{"at least one of headers, methods, path or sourceCIDR must be specified"},
+		},
 		{
 			desc: "panicThreshold is set",
 			mutate: func(btp *egv1a1.BackendTrafficPolicy) {
diff --git a/test/e2e/testdata/backendtrafficpolicy-section-merged.yaml b/test/e2e/testdata/backendtrafficpolicy-section-merged.yaml
new file mode 100644
index 000000000..f6c4d4034
--- /dev/null
+++ b/test/e2e/testdata/backendtrafficpolicy-section-merged.yaml
@@ -0,0 +1,133 @@
+---
+apiVersion: gateway.networking.k8s.io/v1
+kind: Gateway
+metadata:
+  name: btp-section
+  namespace: gateway-conformance-infra
+spec:
+  gatewayClassName: "{GATEWAY_CLASS_NAME}"
+  listeners:
+  - name: listener-1
+    hostname: "listener1.merged.example.com"
+    port: 80
+    protocol: HTTP
+    allowedRoutes:
+      namespaces:
+        from: Same
+  - name: listener-2
+    hostname: "listener2.merged.example.com"
+    port: 80
+    protocol: HTTP
+    allowedRoutes:
+      namespaces:
+        from: Same
+---
+apiVersion: gateway.networking.k8s.io/v1
+kind: HTTPRoute
+metadata:
+  name: btp-section
+  namespace: gateway-conformance-infra
+spec:
+  parentRefs:
+  - name: btp-section
+    sectionName: listener-1
+  hostnames: ["listener1.merged.example.com"]
+  rules:
+  - name: rule-1
+    matches:
+    - path:
+        value: "/foo"
+    backendRefs:
+    - name: infra-backend-v1
+      port: 8080
+  - name: rule-2
+    matches:
+    - path:
+        value: "/bar"
+    backendRefs:
+    - name: infra-backend-v1
+      port: 8080
+---
+apiVersion: gateway.networking.k8s.io/v1
+kind: HTTPRoute
+metadata:
+  name: btp-non-section
+  namespace: gateway-conformance-infra
+spec:
+  parentRefs:
+  - name: btp-section
+    sectionName: listener-2
+  hostnames: ["listener2.merged.example.com"]
+  rules:
+  - matches:
+    - path:
+        value: "/foo"
+    backendRefs:
+    - name: infra-backend-v1
+      port: 8080
+---
+apiVersion: gateway.envoyproxy.io/v1alpha1
+kind: BackendTrafficPolicy
+metadata:
+  name: btp-section-gateway
+  namespace: gateway-conformance-infra
+spec:
+  targetRefs:
+  - group: gateway.networking.k8s.io
+    kind: Gateway
+    name: btp-section
+    sectionName: listener-1
+  responseOverride:
+  - match:
+      statusCodes:
+      - type: Range
+        range:
+          start: 418
+          end: 420
+    response:
+      statusCode: 500
+---
+apiVersion: gateway.envoyproxy.io/v1alpha1
+kind: BackendTrafficPolicy
+metadata:
+  name: btp-section-route
+  namespace: gateway-conformance-infra
+spec:
+  targetRefs:
+  - group: gateway.networking.k8s.io
+    kind: HTTPRoute
+    name: btp-section
+  mergeType: StrategicMerge
+  faultInjection:
+    abort:
+      httpStatus: 418
+---
+apiVersion: gateway.envoyproxy.io/v1alpha1
+kind: BackendTrafficPolicy
+metadata:
+  name: btp-section-route-rule
+  namespace: gateway-conformance-infra
+spec:
+  targetRefs:
+  - group: gateway.networking.k8s.io
+    kind: HTTPRoute
+    name: btp-section
+    sectionName: rule-2
+  faultInjection:
+    abort:
+      httpStatus: 419
+---
+apiVersion: gateway.envoyproxy.io/v1alpha1
+kind: BackendTrafficPolicy
+metadata:
+  name: btp-non-section-route
+  namespace: gateway-conformance-infra
+spec:
+  targetRefs:
+  - group: gateway.networking.k8s.io
+    kind: HTTPRoute
+    name: btp-non-section
+  mergeType: StrategicMerge
+  faultInjection:
+    abort:
+      httpStatus: 420
diff --git a/test/e2e/testdata/cors-http-cors-filter.yaml b/test/e2e/testdata/cors-http-cors-filter.yaml
index 6f42f1880..812eb0f0a 100644
--- a/test/e2e/testdata/cors-http-cors-filter.yaml
+++ b/test/e2e/testdata/cors-http-cors-filter.yaml
@@ -62,3 +62,34 @@ spec:
         - "*"
         exposeHeaders:
         - "*"
+---
+# HTTPRoute with CORS filter and method match to verify that OPTIONS method is implicitly allowed for CORS preflight
+apiVersion: gateway.networking.k8s.io/v1
+kind: HTTPRoute
+metadata:
+  name: http-with-cors-specific-method
+  namespace: gateway-conformance-infra
+spec:
+  parentRefs:
+  - name: same-namespace
+  rules:
+  - matches:
+    - path:
+        type: PathPrefix
+        value: /cors-specific-method
+      method: GET
+    backendRefs:
+    - name: infra-backend-v1
+      port: 8080
+    filters:
+    - type: CORS
+      cors:
+        allowOrigins:
+        - "https://www.foo.com"
+        allowMethods:
+        - GET
+        - OPTIONS
+        allowHeaders:
+        - "*"
+        exposeHeaders:
+        - "*"
diff --git a/test/e2e/testdata/local-ratelimit.yaml b/test/e2e/testdata/local-ratelimit.yaml
index 2840eabb2..dc1232564 100644
--- a/test/e2e/testdata/local-ratelimit.yaml
+++ b/test/e2e/testdata/local-ratelimit.yaml
@@ -150,3 +150,82 @@ spec:
         - path:
             type: Exact
             value: /ratelimit-invert-match
+---
+apiVersion: gateway.envoyproxy.io/v1alpha1
+kind: BackendTrafficPolicy
+metadata:
+  name: ratelimit-path-match
+  namespace: gateway-conformance-infra
+spec:
+  targetRefs:
+    - group: gateway.networking.k8s.io
+      kind: HTTPRoute
+      name: http-ratelimit-path-match
+  rateLimit:
+    type: Local
+    local:
+      rules:
+        - clientSelectors:
+            - path:
+                type: Exact
+                value: /ratelimit-path-match/foo
+          limit:
+            requests: 3
+            unit: Hour
+---
+apiVersion: gateway.networking.k8s.io/v1
+kind: HTTPRoute
+metadata:
+  name: http-ratelimit-path-match
+  namespace: gateway-conformance-infra
+spec:
+  parentRefs:
+    - name: same-namespace
+    - name: all-namespaces
+  rules:
+    - backendRefs:
+        - name: infra-backend-v1
+          port: 8080
+      matches:
+        - path:
+            type: PathPrefix
+            value: /ratelimit-path-match
+---
+apiVersion: gateway.envoyproxy.io/v1alpha1
+kind: BackendTrafficPolicy
+metadata:
+  name: ratelimit-method-match
+  namespace: gateway-conformance-infra
+spec:
+  targetRefs:
+    - group: gateway.networking.k8s.io
+      kind: HTTPRoute
+      name: http-ratelimit-method-match
+  rateLimit:
+    type: Local
+    local:
+      rules:
+        - clientSelectors:
+            - methods:
+                - value: "GET"
+          limit:
+            requests: 3
+            unit: Hour
+---
+apiVersion: gateway.networking.k8s.io/v1
+kind: HTTPRoute
+metadata:
+  name: http-ratelimit-method-match
+  namespace: gateway-conformance-infra
+spec:
+  parentRefs:
+    - name: same-namespace
+    - name: all-namespaces
+  rules:
+    - backendRefs:
+        - name: infra-backend-v1
+          port: 8080
+      matches:
+        - path:
+            type: PathPrefix
+            value: /ratelimit-method-match
diff --git a/test/e2e/testdata/ratelimit-method-match.yaml b/test/e2e/testdata/ratelimit-method-match.yaml
new file mode 100644
index 000000000..7c53c492a
--- /dev/null
+++ b/test/e2e/testdata/ratelimit-method-match.yaml
@@ -0,0 +1,38 @@
+apiVersion: gateway.envoyproxy.io/v1alpha1
+kind: BackendTrafficPolicy
+metadata:
+  name: ratelimit-method-match
+  namespace: gateway-conformance-infra
+spec:
+  targetRefs:
+    - group: gateway.networking.k8s.io
+      kind: HTTPRoute
+      name: method-ratelimit
+  rateLimit:
+    type: Global
+    global:
+      rules:
+        - clientSelectors:
+            - methods:
+                - value: "POST"
+                - value: "DELETE"
+          limit:
+            requests: 3
+            unit: Hour
+---
+apiVersion: gateway.networking.k8s.io/v1
+kind: HTTPRoute
+metadata:
+  name: method-ratelimit
+  namespace: gateway-conformance-infra
+spec:
+  parentRefs:
+    - name: same-namespace
+  rules:
+    - matches:
+        - path:
+            type: PathPrefix
+            value: /get
+      backendRefs:
+        - name: infra-backend-v1
+          port: 8080
diff --git a/test/e2e/testdata/ratelimit-path-match.yaml b/test/e2e/testdata/ratelimit-path-match.yaml
new file mode 100644
index 000000000..38e874da9
--- /dev/null
+++ b/test/e2e/testdata/ratelimit-path-match.yaml
@@ -0,0 +1,38 @@
+apiVersion: gateway.envoyproxy.io/v1alpha1
+kind: BackendTrafficPolicy
+metadata:
+  name: ratelimit-path-match
+  namespace: gateway-conformance-infra
+spec:
+  targetRefs:
+    - group: gateway.networking.k8s.io
+      kind: HTTPRoute
+      name: path-ratelimit
+  rateLimit:
+    type: Global
+    global:
+      rules:
+        - clientSelectors:
+            - path:
+                type: PathPrefix
+                value: "/get/specific-path"
+          limit:
+            requests: 3
+            unit: Hour
+---
+apiVersion: gateway.networking.k8s.io/v1
+kind: HTTPRoute
+metadata:
+  name: path-ratelimit
+  namespace: gateway-conformance-infra
+spec:
+  parentRefs:
+    - name: same-namespace
+  rules:
+    - matches:
+        - path:
+            type: PathPrefix
+            value: /get
+      backendRefs:
+        - name: infra-backend-v1
+          port: 8080
diff --git a/test/e2e/tests/backendtrafficpolicy_section_merged.go b/test/e2e/tests/backendtrafficpolicy_section_merged.go
new file mode 100644
index 000000000..4351826b0
--- /dev/null
+++ b/test/e2e/tests/backendtrafficpolicy_section_merged.go
@@ -0,0 +1,133 @@
+// Copyright Envoy Gateway Authors
+// SPDX-License-Identifier: Apache-2.0
+// The full text of the Apache license is available in the LICENSE file at
+// the root of the repo.
+
+//go:build e2e
+
+package tests
+
+import (
+	"testing"
+
+	"k8s.io/apimachinery/pkg/types"
+	"k8s.io/utils/ptr"
+	gwapiv1 "sigs.k8s.io/gateway-api/apis/v1"
+	gwapiv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
+	"sigs.k8s.io/gateway-api/conformance/utils/http"
+	"sigs.k8s.io/gateway-api/conformance/utils/kubernetes"
+	"sigs.k8s.io/gateway-api/conformance/utils/suite"
+
+	"github.com/envoyproxy/gateway/internal/gatewayapi"
+	"github.com/envoyproxy/gateway/internal/gatewayapi/resource"
+)
+
+func init() {
+	ConformanceTests = append(ConformanceTests, BackendTrafficPolicyMergedTest)
+}
+
+var BackendTrafficPolicyMergedTest = suite.ConformanceTest{
+	ShortName:   "BackendTrafficPolicyMerged",
+	Description: "Test section level policy attach and merged parent policy for BackendTrafficPolicy",
+	Manifests:   []string{"testdata/backendtrafficpolicy-section-merged.yaml"},
+	Test: func(t *testing.T, suite *suite.ConformanceTestSuite) {
+		t.Run("BackendTrafficPolicyMerged", func(t *testing.T) {
+			ns := "gateway-conformance-infra"
+
+			sectionRouteNN := types.NamespacedName{Name: "btp-section", Namespace: ns}
+			nonsectionRouteNN := types.NamespacedName{Name: "btp-non-section", Namespace: ns}
+			gwNN := types.NamespacedName{Name: "btp-section", Namespace: ns}
+
+			gwAddr := kubernetes.GatewayAndHTTPRoutesMustBeAccepted(t,
+				suite.Client,
+				suite.TimeoutConfig,
+				suite.ControllerName,
+				kubernetes.NewGatewayRef(gwNN),
+				sectionRouteNN,
+				nonsectionRouteNN,
+			)
+
+			BackendTrafficPolicyMustBeAccepted(t,
+				suite.Client,
+				types.NamespacedName{Name: "btp-section-gateway", Namespace: ns},
+				suite.ControllerName,
+				gwapiv1a2.ParentReference{
+					Group:       gatewayapi.GroupPtr(gwapiv1.GroupName),
+					Kind:        gatewayapi.KindPtr(resource.KindGateway),
+					Namespace:   gatewayapi.NamespacePtr(gwNN.Namespace),
+					Name:        gwapiv1.ObjectName(gwNN.Name),
+					SectionName: ptr.To(gwapiv1.SectionName("listener-1")),
+				},
+			)
+
+			BackendTrafficPolicyMustBeAccepted(t,
+				suite.Client,
+				types.NamespacedName{Name: "btp-section-route", Namespace: ns},
+				suite.ControllerName,
+				gwapiv1a2.ParentReference{
+					Group:       gatewayapi.GroupPtr(gwapiv1.GroupName),
+					Kind:        gatewayapi.KindPtr(resource.KindGateway),
+					Namespace:   gatewayapi.NamespacePtr(gwNN.Namespace),
+					Name:        gwapiv1.ObjectName(gwNN.Name),
+					SectionName: ptr.To(gwapiv1.SectionName("listener-1")),
+				},
+			)
+
+			BackendTrafficPolicyMustBeAccepted(t,
+				suite.Client,
+				types.NamespacedName{Name: "btp-section-route-rule", Namespace: ns},
+				suite.ControllerName,
+				gwapiv1a2.ParentReference{
+					Group:       gatewayapi.GroupPtr(gwapiv1.GroupName),
+					Kind:        gatewayapi.KindPtr(resource.KindGateway),
+					Namespace:   gatewayapi.NamespacePtr(gwNN.Namespace),
+					Name:        gwapiv1.ObjectName(gwNN.Name),
+					SectionName: ptr.To(gwapiv1.SectionName("listener-1")),
+				},
+			)
+
+			BackendTrafficPolicyMustBeAccepted(t,
+				suite.Client,
+				types.NamespacedName{Name: "btp-non-section-route", Namespace: ns},
+				suite.ControllerName,
+				gwapiv1a2.ParentReference{
+					Group:       gatewayapi.GroupPtr(gwapiv1.GroupName),
+					Kind:        gatewayapi.KindPtr(resource.KindGateway),
+					Namespace:   gatewayapi.NamespacePtr(gwNN.Namespace),
+					Name:        gwapiv1.ObjectName(gwNN.Name),
+					SectionName: ptr.To(gwapiv1.SectionName("listener-2")),
+				},
+			)
+
+			// Test 1: HTTPRoute btp-section/rule-2 with section-level FaultInjection (419)
+			// This rule has its own BTP without mergeType, so Gateway's ResponseOverride doesn't apply.
+			// Expected: StatusCode 419 (FaultInjection abort)
+			expectedResponse := http.ExpectedResponse{
+				Namespace: ns,
+				Request:   http.Request{Host: "listener1.merged.example.com", Path: "/bar"},
+				Response:  http.Response{StatusCode: 419},
+			}
+			http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, expectedResponse)
+
+			// Test 2: HTTPRoute btp-section/rule-1 with merged BTP (FaultInjection 418 + ResponseOverride)
+			// Route-level FaultInjection (418) with mergeType merges with Gateway's ResponseOverride (418->500).
+			// Expected: StatusCode 500 (ResponseOverride transforms 418 to 500)
+			expectedResponse = http.ExpectedResponse{
+				Namespace: ns,
+				Request:   http.Request{Host: "listener1.merged.example.com", Path: "/foo"},
+				Response:  http.Response{StatusCode: 500},
+			}
+			http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, expectedResponse)
+
+			// Test 3: HTTPRoute btp-non-section on listener-2 with merged BTP (FaultInjection 420)
+			// This route uses listener-2 which doesn't have Gateway ResponseOverride (listener-1 only).
+			// Expected: StatusCode 420 (FaultInjection abort, no ResponseOverride applied)
+			expectedResponse = http.ExpectedResponse{
+				Namespace: ns,
+				Request:   http.Request{Host: "listener2.merged.example.com", Path: "/foo"},
+				Response:  http.Response{StatusCode: 420},
+			}
+			http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, expectedResponse)
+		})
+	},
+}
diff --git a/test/e2e/tests/cors.go b/test/e2e/tests/cors.go
index f2421d37d..9112ae7b5 100644
--- a/test/e2e/tests/cors.go
+++ b/test/e2e/tests/cors.go
@@ -202,4 +202,44 @@ func runCORStest(t *testing.T, suite *suite.ConformanceTestSuite, withSecurityPo
 
 		http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, expectedResponse)
 	})
+
+	if !withSecurityPolicy {
+		t.Run("should enable cors with specific method match", func(t *testing.T) {
+			// When using HTTPCORSFilter with method match, OPTIONS method is implicitly allowed for CORS preflight
+			// even without explicit OPTIONS method match, so preflight response should be returned successfully
+			expectedResponse := http.ExpectedResponse{
+				Request: http.Request{
+					Path:   "/cors-specific-method",
+					Method: "OPTIONS",
+					Headers: map[string]string{
+						"Origin":                         "https://www.foo.com",
+						"access-control-request-method":  "GET",
+						"access-control-request-headers": "x-header-1, x-header-2",
+					},
+				},
+				// Set the expected request properties to empty strings.
+				// This is a workaround to avoid the test failure.
+				// The response body is empty because the request is a preflight request.
+				ExpectedRequest: &http.ExpectedRequest{
+					Request: http.Request{
+						Host:    "",
+						Method:  "OPTIONS",
+						Path:    "",
+						Headers: nil,
+					},
+				},
+				Response: http.Response{
+					StatusCode: 200,
+					Headers: map[string]string{
+						"access-control-allow-origin":   "https://www.foo.com",
+						"access-control-allow-methods":  "GET, OPTIONS",
+						"access-control-allow-headers":  "x-header-1, x-header-2",
+						"access-control-expose-headers": "*",
+					},
+				},
+				Namespace: "",
+			}
+			http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, expectedResponse)
+		})
+	}
 }
diff --git a/test/e2e/tests/local_ratelimit.go b/test/e2e/tests/local_ratelimit.go
index a7c06b414..2b9543b31 100644
--- a/test/e2e/tests/local_ratelimit.go
+++ b/test/e2e/tests/local_ratelimit.go
@@ -60,6 +60,14 @@ var LocalRateLimitTest = suite.ConformanceTest{
 			t.Run(fmt.Sprintf("HeaderInvertMatch-%s", caseSuffix), func(t *testing.T) {
 				runHeaderInvertMatchRateLimitTest(t, suite, disableHeader)
 			})
+
+			t.Run(fmt.Sprintf("PathMatch-%s", caseSuffix), func(t *testing.T) {
+				runPathMatchRateLimitTest(t, suite, disableHeader)
+			})
+
+			t.Run(fmt.Sprintf("MethodMatch-%s", caseSuffix), func(t *testing.T) {
+				runMethodMatchRateLimitTest(t, suite, disableHeader)
+			})
 		}
 	},
 }
@@ -361,3 +369,138 @@ func runHeaderInvertMatchRateLimitTest(t *testing.T, suite *suite.ConformanceTes
 	}
 	http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, testOrgResponse)
 }
+
+func runPathMatchRateLimitTest(t *testing.T, suite *suite.ConformanceTestSuite, disableHeader bool) {
+	ns := "gateway-conformance-infra"
+	gwNN := gatewayNN(disableHeader)
+	gwAddr := gatewayAndHTTPRoutesMustBeAccepted(t, suite, gwNN)
+
+	ancestorRef := gwapiv1.ParentReference{
+		Group:     gatewayapi.GroupPtr(gwapiv1.GroupName),
+		Kind:      gatewayapi.KindPtr(resource.KindGateway),
+		Namespace: gatewayapi.NamespacePtr(gwNN.Namespace),
+		Name:      gwapiv1.ObjectName(gwNN.Name),
+	}
+	BackendTrafficPolicyMustBeAccepted(t, suite.Client, types.NamespacedName{Name: "ratelimit-path-match", Namespace: ns}, suite.ControllerName, ancestorRef)
+
+	okResponse := http.ExpectedResponse{
+		Request: http.Request{
+			Path: "/ratelimit-path-match/foo",
+		},
+		Response: http.Response{
+			StatusCode: 200,
+		},
+		Namespace: ns,
+	}
+	if !disableHeader {
+		okResponse.Response.Headers = map[string]string{
+			RatelimitLimitHeaderName:     "3",
+			RatelimitRemainingHeaderName: "1",
+			RatelimitResetHeaderName:     "0",
+		}
+	} else {
+		okResponse.Response.AbsentHeaders = allRateLimitHeaders
+	}
+	// keep sending requests till get 200 first, that will cost one 200
+	http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, okResponse)
+
+	limitResponse := http.ExpectedResponse{
+		Request: http.Request{
+			Path: "/ratelimit-path-match/foo",
+		},
+		Response: http.Response{
+			StatusCode: 429,
+		},
+		Namespace: ns,
+	}
+	if !disableHeader {
+		limitResponse.Response.Headers = map[string]string{
+			RatelimitLimitHeaderName:     "3",
+			RatelimitRemainingHeaderName: "0", // at the end the remaining should be 0
+		}
+	} else {
+		limitResponse.Response.AbsentHeaders = allRateLimitHeaders
+	}
+	// this request should be limited at the end
+	http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, limitResponse)
+
+	okResponse = http.ExpectedResponse{
+		Request: http.Request{
+			Path: "/ratelimit-path-match/bar",
+		},
+		Response: http.Response{
+			StatusCode: 200,
+		},
+		Namespace: ns,
+	}
+	// this request should not be limited
+	http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, okResponse)
+}
+
+func runMethodMatchRateLimitTest(t *testing.T, suite *suite.ConformanceTestSuite, disableHeader bool) {
+	ns := "gateway-conformance-infra"
+	gwNN := gatewayNN(disableHeader)
+	gwAddr := gatewayAndHTTPRoutesMustBeAccepted(t, suite, gwNN)
+
+	ancestorRef := gwapiv1.ParentReference{
+		Group:     gatewayapi.GroupPtr(gwapiv1.GroupName),
+		Kind:      gatewayapi.KindPtr(resource.KindGateway),
+		Namespace: gatewayapi.NamespacePtr(gwNN.Namespace),
+		Name:      gwapiv1.ObjectName(gwNN.Name),
+	}
+	BackendTrafficPolicyMustBeAccepted(t, suite.Client, types.NamespacedName{Name: "ratelimit-method-match", Namespace: ns}, suite.ControllerName, ancestorRef)
+
+	okResponse := http.ExpectedResponse{
+		Request: http.Request{
+			Path: "/ratelimit-method-match",
+		},
+		Response: http.Response{
+			StatusCode: 200,
+		},
+		Namespace: ns,
+	}
+	if !disableHeader {
+		okResponse.Response.Headers = map[string]string{
+			RatelimitLimitHeaderName:     "3",
+			RatelimitRemainingHeaderName: "1",
+			RatelimitResetHeaderName:     "0",
+		}
+	} else {
+		okResponse.Response.AbsentHeaders = allRateLimitHeaders
+	}
+	// keep sending requests till get 200 first, that will cost one 200
+	http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, okResponse)
+
+	limitResponse := http.ExpectedResponse{
+		Request: http.Request{
+			Path: "/ratelimit-method-match",
+		},
+		Response: http.Response{
+			StatusCode: 429,
+		},
+		Namespace: ns,
+	}
+	if !disableHeader {
+		limitResponse.Response.Headers = map[string]string{
+			RatelimitLimitHeaderName:     "3",
+			RatelimitRemainingHeaderName: "0", // at the end the remaining should be 0
+		}
+	} else {
+		limitResponse.Response.AbsentHeaders = allRateLimitHeaders
+	}
+	// this request should be limited at the end
+	http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, limitResponse)
+
+	okResponse = http.ExpectedResponse{
+		Request: http.Request{
+			Path:   "/ratelimit-method-match",
+			Method: "POST",
+		},
+		Response: http.Response{
+			StatusCode: 200,
+		},
+		Namespace: ns,
+	}
+	// this request should not be limited
+	http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, okResponse)
+}
diff --git a/test/e2e/tests/ratelimit.go b/test/e2e/tests/ratelimit.go
index 06b80e405..8d5b1cefa 100644
--- a/test/e2e/tests/ratelimit.go
+++ b/test/e2e/tests/ratelimit.go
@@ -33,6 +33,8 @@ func init() {
 	ConformanceTests = append(ConformanceTests,
 		RateLimitCIDRMatchTest,
 		RateLimitHeaderMatchTest,
+		RateLimitMethodMatchTest,
+		RateLimitPathMatchTest,
 		GlobalRateLimitHeaderInvertMatchTest,
 		RateLimitHeadersDisabled,
 		RateLimitBasedJwtClaimsTest,
@@ -111,6 +113,179 @@ var RateLimitCIDRMatchTest = suite.ConformanceTest{
 	},
 }
 
+var RateLimitMethodMatchTest = suite.ConformanceTest{
+	ShortName:   "RateLimitMethodMatch",
+	Description: "Limit all requests that match method",
+	Manifests:   []string{"testdata/ratelimit-method-match.yaml"},
+	Test: func(t *testing.T, suite *suite.ConformanceTestSuite) {
+		ns := "gateway-conformance-infra"
+		routeNN := types.NamespacedName{Name: "method-ratelimit", Namespace: ns}
+		gwNN := types.NamespacedName{Name: "same-namespace", Namespace: ns}
+		gwAddr := kubernetes.GatewayAndRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), &gwapiv1.HTTPRoute{}, false, routeNN)
+
+		t.Run("matched method can got limited", func(t *testing.T) {
+			ratelimitHeader := make(map[string]string)
+
+			expectOkResp := http.ExpectedResponse{
+				Request: http.Request{
+					Path:   "/get",
+					Method: "POST",
+				},
+				Response: http.Response{
+					StatusCode: 200,
+					Headers:    ratelimitHeader,
+				},
+				Namespace: ns,
+			}
+			expectOkResp.Response.Headers["X-Ratelimit-Limit"] = "3, 3;w=3600"
+			expectOkReq := http.MakeRequest(t, &expectOkResp, gwAddr, "HTTP", "http")
+
+			expectLimitResp := http.ExpectedResponse{
+				Request: http.Request{
+					Path:   "/get",
+					Method: "POST",
+				},
+				Response: http.Response{
+					StatusCode: 429,
+				},
+				Namespace: ns,
+			}
+			expectLimitReq := http.MakeRequest(t, &expectLimitResp, gwAddr, "HTTP", "http")
+
+			expectLimitDeleteReq := http.ExpectedResponse{
+				Request: http.Request{
+					Path:   "/get",
+					Method: "DELETE",
+				},
+				Response: http.Response{
+					StatusCode: 429,
+				},
+				Namespace: ns,
+			}
+			expectLimitDeleteResp := http.MakeRequest(t, &expectLimitDeleteReq, gwAddr, "HTTP", "http")
+
+			// should just send exactly 4 requests, and expect 429
+
+			// keep sending requests till get 200 first, that will cost one 200
+			http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, expectOkResp)
+
+			// fire the rest of the requests
+			if err := GotExactExpectedResponse(t, 2, suite.RoundTripper, expectOkReq, expectOkResp); err != nil {
+				t.Errorf("failed to get expected response for the first three requests: %v", err)
+			}
+			if err := GotExactExpectedResponse(t, 1, suite.RoundTripper, expectLimitReq, expectLimitResp); err != nil {
+				t.Errorf("failed to get expected response for the last (fourth) request: %v", err)
+			}
+			// Since POST method already consumed the rate limit quota,
+			// DELETE request should be immediately rate limited (both methods share the same counter).
+			if err := GotExactExpectedResponse(t, 1, suite.RoundTripper, expectLimitDeleteResp, expectLimitDeleteReq); err != nil {
+				t.Errorf("failed to get expected response for the limited delete request: %v", err)
+			}
+		})
+
+		t.Run("not matched method cannot got limited", func(t *testing.T) {
+			// it does not require any rate limit header, since this request never be rate limited.
+			expectOkResp := http.ExpectedResponse{
+				Request: http.Request{
+					Path: "/get",
+				},
+				Response: http.Response{
+					StatusCode: 200,
+				},
+				Namespace: ns,
+			}
+			expectOkReq := http.MakeRequest(t, &expectOkResp, gwAddr, "HTTP", "http")
+
+			// send exactly 4 requests, and still expect 200
+
+			// keep sending requests till get 200 first, that will cost one 200
+			http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, expectOkResp)
+
+			// fire the rest of the requests
+			if err := GotExactExpectedResponse(t, 3, suite.RoundTripper, expectOkReq, expectOkResp); err != nil {
+				t.Errorf("failed to get expected responses for the request: %v", err)
+			}
+		})
+	},
+}
+
+var RateLimitPathMatchTest = suite.ConformanceTest{
+	ShortName:   "RateLimitPathMatch",
+	Description: "Limit all requests that match path",
+	Manifests:   []string{"testdata/ratelimit-path-match.yaml"},
+	Test: func(t *testing.T, suite *suite.ConformanceTestSuite) {
+		ns := "gateway-conformance-infra"
+		routeNN := types.NamespacedName{Name: "path-ratelimit", Namespace: ns}
+		gwNN := types.NamespacedName{Name: "same-namespace", Namespace: ns}
+		gwAddr := kubernetes.GatewayAndRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), &gwapiv1.HTTPRoute{}, false, routeNN)
+
+		t.Run("matched path can got limited", func(t *testing.T) {
+			ratelimitHeader := make(map[string]string)
+
+			expectOkResp := http.ExpectedResponse{
+				Request: http.Request{
+					Path: "/get/specific-path",
+				},
+				Response: http.Response{
+					StatusCode: 200,
+					Headers:    ratelimitHeader,
+				},
+				Namespace: ns,
+			}
+			expectOkResp.Response.Headers["X-Ratelimit-Limit"] = "3, 3;w=3600"
+			expectOkReq := http.MakeRequest(t, &expectOkResp, gwAddr, "HTTP", "http")
+
+			expectLimitResp := http.ExpectedResponse{
+				Request: http.Request{
+					Path: "/get/specific-path",
+				},
+				Response: http.Response{
+					StatusCode: 429,
+				},
+				Namespace: ns,
+			}
+			expectLimitReq := http.MakeRequest(t, &expectLimitResp, gwAddr, "HTTP", "http")
+
+			// should just send exactly 4 requests, and expect 429
+
+			// keep sending requests till get 200 first, that will cost one 200
+			http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, expectOkResp)
+
+			// fire the rest of the requests
+			if err := GotExactExpectedResponse(t, 2, suite.RoundTripper, expectOkReq, expectOkResp); err != nil {
+				t.Errorf("failed to get expected response for the first three requests: %v", err)
+			}
+			if err := GotExactExpectedResponse(t, 1, suite.RoundTripper, expectLimitReq, expectLimitResp); err != nil {
+				t.Errorf("failed to get expected response for the last (fourth) request: %v", err)
+			}
+		})
+
+		t.Run("not matched path cannot got limited", func(t *testing.T) {
+			// it does not require any rate limit header, since this request never be rate limited.
+			expectOkResp := http.ExpectedResponse{
+				Request: http.Request{
+					Path: "/get/no-specific-path",
+				},
+				Response: http.Response{
+					StatusCode: 200,
+				},
+				Namespace: ns,
+			}
+			expectOkReq := http.MakeRequest(t, &expectOkResp, gwAddr, "HTTP", "http")
+
+			// send exactly 4 requests, and still expect 200
+
+			// keep sending requests till get 200 first, that will cost one 200
+			http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, expectOkResp)
+
+			// fire the rest of the requests
+			if err := GotExactExpectedResponse(t, 3, suite.RoundTripper, expectOkReq, expectOkResp); err != nil {
+				t.Errorf("failed to get expected responses for the request: %v", err)
+			}
+		})
+	},
+}
+
 var RateLimitHeaderMatchTest = suite.ConformanceTest{
 	ShortName:   "RateLimitHeaderMatch",
 	Description: "Limit all requests that match headers",
diff --git a/test/helm/gateway-crds-helm/all.out.yaml b/test/helm/gateway-crds-helm/all.out.yaml
index 8163bb666..6e90da5cf 100644
--- a/test/helm/gateway-crds-helm/all.out.yaml
+++ b/test/helm/gateway-crds-helm/all.out.yaml
@@ -22081,12 +22081,12 @@ spec:
                                   RateLimitSelectCondition specifies the attributes within the traffic flow that can
                                   be used to select a subset of clients to be ratelimited.
                                   All the individual conditions must hold True for the overall condition to hold True.
+                                  And, at least one of headers or methods or path or sourceCIDR condition must be specified.
                                 properties:
                                   headers:
                                     description: |-
                                       Headers is a list of request headers to match. Multiple header values are ANDed together,
                                       meaning, a request MUST match all the specified headers.
-                                      At least one of headers or sourceCIDR condition must be specified.
                                     items:
                                       description: HeaderMatch defines the match attributes
                                         within the HTTP Headers of the request.
@@ -22127,10 +22127,66 @@ spec:
                                       type: object
                                     maxItems: 16
                                     type: array
-                                  sourceCIDR:
+                                  methods:
+                                    description: |-
+                                      Methods is a list of request methods to match. Multiple method values are ORed together,
+                                      meaning, a request can match any one of the specified methods. If not specified, it matches all methods.
+                                    items:
+                                      description: MethodMatch defines the matching
+                                        criteria for the HTTP method of a request.
+                                      properties:
+                                        invert:
+                                          default: false
+                                          description: Invert specifies whether the
+                                            value match result will be inverted.
+                                          type: boolean
+                                        value:
+                                          description: Value specifies the HTTP method.
+                                          enum:
+                                          - GET
+                                          - HEAD
+                                          - POST
+                                          - PUT
+                                          - DELETE
+                                          - CONNECT
+                                          - OPTIONS
+                                          - TRACE
+                                          - PATCH
+                                          type: string
+                                      required:
+                                      - value
+                                      type: object
+                                    type: array
+                                  path:
                                     description: |-
-                                      SourceCIDR is the client IP Address range to match on.
-                                      At least one of headers or sourceCIDR condition must be specified.
+                                      Path is the request path to match.
+                                      Support Exact, PathPrefix and RegularExpression match types.
+                                    properties:
+                                      invert:
+                                        default: false
+                                        description: Invert specifies whether the
+                                          value match result will be inverted.
+                                        type: boolean
+                                      type:
+                                        default: PathPrefix
+                                        description: Type specifies how to match against
+                                          the value of the path.
+                                        enum:
+                                        - Exact
+                                        - PathPrefix
+                                        - RegularExpression
+                                        type: string
+                                      value:
+                                        default: /
+                                        description: Value specifies the HTTP path.
+                                        maxLength: 1024
+                                        type: string
+                                    required:
+                                    - value
+                                    type: object
+                                  sourceCIDR:
+                                    description: SourceCIDR is the client IP Address
+                                      range to match on.
                                     properties:
                                       type:
                                         default: Exact
@@ -22150,6 +22206,11 @@ spec:
                                     - value
                                     type: object
                                 type: object
+                                x-kubernetes-validations:
+                                - message: at least one of headers, methods, path
+                                    or sourceCIDR must be specified
+                                  rule: has(self.headers) || has(self.methods) ||
+                                    has(self.path) || has(self.sourceCIDR)
                               maxItems: 8
                               type: array
                             cost:
@@ -22332,12 +22393,12 @@ spec:
                                   RateLimitSelectCondition specifies the attributes within the traffic flow that can
                                   be used to select a subset of clients to be ratelimited.
                                   All the individual conditions must hold True for the overall condition to hold True.
+                                  And, at least one of headers or methods or path or sourceCIDR condition must be specified.
                                 properties:
                                   headers:
                                     description: |-
                                       Headers is a list of request headers to match. Multiple header values are ANDed together,
                                       meaning, a request MUST match all the specified headers.
-                                      At least one of headers or sourceCIDR condition must be specified.
                                     items:
                                       description: HeaderMatch defines the match attributes
                                         within the HTTP Headers of the request.
@@ -22378,10 +22439,66 @@ spec:
                                       type: object
                                     maxItems: 16
                                     type: array
-                                  sourceCIDR:
+                                  methods:
                                     description: |-
-                                      SourceCIDR is the client IP Address range to match on.
-                                      At least one of headers or sourceCIDR condition must be specified.
+                                      Methods is a list of request methods to match. Multiple method values are ORed together,
+                                      meaning, a request can match any one of the specified methods. If not specified, it matches all methods.
+                                    items:
+                                      description: MethodMatch defines the matching
+                                        criteria for the HTTP method of a request.
+                                      properties:
+                                        invert:
+                                          default: false
+                                          description: Invert specifies whether the
+                                            value match result will be inverted.
+                                          type: boolean
+                                        value:
+                                          description: Value specifies the HTTP method.
+                                          enum:
+                                          - GET
+                                          - HEAD
+                                          - POST
+                                          - PUT
+                                          - DELETE
+                                          - CONNECT
+                                          - OPTIONS
+                                          - TRACE
+                                          - PATCH
+                                          type: string
+                                      required:
+                                      - value
+                                      type: object
+                                    type: array
+                                  path:
+                                    description: |-
+                                      Path is the request path to match.
+                                      Support Exact, PathPrefix and RegularExpression match types.
+                                    properties:
+                                      invert:
+                                        default: false
+                                        description: Invert specifies whether the
+                                          value match result will be inverted.
+                                        type: boolean
+                                      type:
+                                        default: PathPrefix
+                                        description: Type specifies how to match against
+                                          the value of the path.
+                                        enum:
+                                        - Exact
+                                        - PathPrefix
+                                        - RegularExpression
+                                        type: string
+                                      value:
+                                        default: /
+                                        description: Value specifies the HTTP path.
+                                        maxLength: 1024
+                                        type: string
+                                    required:
+                                    - value
+                                    type: object
+                                  sourceCIDR:
+                                    description: SourceCIDR is the client IP Address
+                                      range to match on.
                                     properties:
                                       type:
                                         default: Exact
@@ -22401,6 +22518,11 @@ spec:
                                     - value
                                     type: object
                                 type: object
+                                x-kubernetes-validations:
+                                - message: at least one of headers, methods, path
+                                    or sourceCIDR must be specified
+                                  rule: has(self.headers) || has(self.methods) ||
+                                    has(self.path) || has(self.sourceCIDR)
                               maxItems: 8
                               type: array
                             cost:
@@ -23427,8 +23549,6 @@ spec:
             - message: this policy can only have a targetRef.kind of Gateway/HTTPRoute/GRPCRoute/TCPRoute/UDPRoute/TLSRoute
               rule: 'has(self.targetRef) ? self.targetRef.kind in [''Gateway'', ''HTTPRoute'',
                 ''GRPCRoute'', ''UDPRoute'', ''TCPRoute'', ''TLSRoute''] : true'
-            - message: this policy does not yet support the sectionName field
-              rule: 'has(self.targetRef) ? !has(self.targetRef.sectionName) : true'
             - message: this policy can only have a targetRefs[*].group of gateway.networking.k8s.io
               rule: 'has(self.targetRefs) ? self.targetRefs.all(ref, ref.group ==
                 ''gateway.networking.k8s.io'') : true '
@@ -23436,9 +23556,6 @@ spec:
               rule: 'has(self.targetRefs) ? self.targetRefs.all(ref, ref.kind in [''Gateway'',
                 ''HTTPRoute'', ''GRPCRoute'', ''UDPRoute'', ''TCPRoute'', ''TLSRoute''])
                 : true '
-            - message: this policy does not yet support the sectionName field
-              rule: 'has(self.targetRefs) ? self.targetRefs.all(ref, !has(ref.sectionName))
-                : true'
             - message: either compression or compressor can be set, not both
               rule: '!has(self.compression) || !has(self.compressor)'
             - message: predictivePercent in preconnect policy only works with RoundRobin
@@ -24276,12 +24393,13 @@ spec:
                       PreserveXRequestID configures Envoy to keep the X-Request-ID header if passed for a request that is edge
                       (Edge request is the request from external clients to front Envoy) and not reset it, which is the current Envoy behaviour.
                       Defaults to false and cannot be combined with RequestID.
-                      Deprecated: use RequestID=Preserve instead
+                      Deprecated: use RequestID=PreserveOrGenerate instead
                     type: boolean
                   requestID:
                     description: |-
                       RequestID configures Envoy's behavior for handling the `X-Request-ID` header.
-                      Defaults to `Generate` and builds the `X-Request-ID` for every request and ignores pre-existing values from the edge.
+                      When omitted default behavior is `Generate` which builds the `X-Request-ID` for every request
+                       and ignores pre-existing values from the edge.
                       (An "edge request" refers to a request from an external client to the Envoy entrypoint.)
                     enum:
                     - PreserveOrGenerate
diff --git a/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml b/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml
index c547fe9c5..9bc4c4c54 100644
--- a/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml
+++ b/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml
@@ -1425,12 +1425,12 @@ spec:
                                   RateLimitSelectCondition specifies the attributes within the traffic flow that can
                                   be used to select a subset of clients to be ratelimited.
                                   All the individual conditions must hold True for the overall condition to hold True.
+                                  And, at least one of headers or methods or path or sourceCIDR condition must be specified.
                                 properties:
                                   headers:
                                     description: |-
                                       Headers is a list of request headers to match. Multiple header values are ANDed together,
                                       meaning, a request MUST match all the specified headers.
-                                      At least one of headers or sourceCIDR condition must be specified.
                                     items:
                                       description: HeaderMatch defines the match attributes
                                         within the HTTP Headers of the request.
@@ -1471,10 +1471,66 @@ spec:
                                       type: object
                                     maxItems: 16
                                     type: array
-                                  sourceCIDR:
+                                  methods:
                                     description: |-
-                                      SourceCIDR is the client IP Address range to match on.
-                                      At least one of headers or sourceCIDR condition must be specified.
+                                      Methods is a list of request methods to match. Multiple method values are ORed together,
+                                      meaning, a request can match any one of the specified methods. If not specified, it matches all methods.
+                                    items:
+                                      description: MethodMatch defines the matching
+                                        criteria for the HTTP method of a request.
+                                      properties:
+                                        invert:
+                                          default: false
+                                          description: Invert specifies whether the
+                                            value match result will be inverted.
+                                          type: boolean
+                                        value:
+                                          description: Value specifies the HTTP method.
+                                          enum:
+                                          - GET
+                                          - HEAD
+                                          - POST
+                                          - PUT
+                                          - DELETE
+                                          - CONNECT
+                                          - OPTIONS
+                                          - TRACE
+                                          - PATCH
+                                          type: string
+                                      required:
+                                      - value
+                                      type: object
+                                    type: array
+                                  path:
+                                    description: |-
+                                      Path is the request path to match.
+                                      Support Exact, PathPrefix and RegularExpression match types.
+                                    properties:
+                                      invert:
+                                        default: false
+                                        description: Invert specifies whether the
+                                          value match result will be inverted.
+                                        type: boolean
+                                      type:
+                                        default: PathPrefix
+                                        description: Type specifies how to match against
+                                          the value of the path.
+                                        enum:
+                                        - Exact
+                                        - PathPrefix
+                                        - RegularExpression
+                                        type: string
+                                      value:
+                                        default: /
+                                        description: Value specifies the HTTP path.
+                                        maxLength: 1024
+                                        type: string
+                                    required:
+                                    - value
+                                    type: object
+                                  sourceCIDR:
+                                    description: SourceCIDR is the client IP Address
+                                      range to match on.
                                     properties:
                                       type:
                                         default: Exact
@@ -1494,6 +1550,11 @@ spec:
                                     - value
                                     type: object
                                 type: object
+                                x-kubernetes-validations:
+                                - message: at least one of headers, methods, path
+                                    or sourceCIDR must be specified
+                                  rule: has(self.headers) || has(self.methods) ||
+                                    has(self.path) || has(self.sourceCIDR)
                               maxItems: 8
                               type: array
                             cost:
@@ -1676,12 +1737,12 @@ spec:
                                   RateLimitSelectCondition specifies the attributes within the traffic flow that can
                                   be used to select a subset of clients to be ratelimited.
                                   All the individual conditions must hold True for the overall condition to hold True.
+                                  And, at least one of headers or methods or path or sourceCIDR condition must be specified.
                                 properties:
                                   headers:
                                     description: |-
                                       Headers is a list of request headers to match. Multiple header values are ANDed together,
                                       meaning, a request MUST match all the specified headers.
-                                      At least one of headers or sourceCIDR condition must be specified.
                                     items:
                                       description: HeaderMatch defines the match attributes
                                         within the HTTP Headers of the request.
@@ -1722,10 +1783,66 @@ spec:
                                       type: object
                                     maxItems: 16
                                     type: array
-                                  sourceCIDR:
+                                  methods:
                                     description: |-
-                                      SourceCIDR is the client IP Address range to match on.
-                                      At least one of headers or sourceCIDR condition must be specified.
+                                      Methods is a list of request methods to match. Multiple method values are ORed together,
+                                      meaning, a request can match any one of the specified methods. If not specified, it matches all methods.
+                                    items:
+                                      description: MethodMatch defines the matching
+                                        criteria for the HTTP method of a request.
+                                      properties:
+                                        invert:
+                                          default: false
+                                          description: Invert specifies whether the
+                                            value match result will be inverted.
+                                          type: boolean
+                                        value:
+                                          description: Value specifies the HTTP method.
+                                          enum:
+                                          - GET
+                                          - HEAD
+                                          - POST
+                                          - PUT
+                                          - DELETE
+                                          - CONNECT
+                                          - OPTIONS
+                                          - TRACE
+                                          - PATCH
+                                          type: string
+                                      required:
+                                      - value
+                                      type: object
+                                    type: array
+                                  path:
+                                    description: |-
+                                      Path is the request path to match.
+                                      Support Exact, PathPrefix and RegularExpression match types.
+                                    properties:
+                                      invert:
+                                        default: false
+                                        description: Invert specifies whether the
+                                          value match result will be inverted.
+                                        type: boolean
+                                      type:
+                                        default: PathPrefix
+                                        description: Type specifies how to match against
+                                          the value of the path.
+                                        enum:
+                                        - Exact
+                                        - PathPrefix
+                                        - RegularExpression
+                                        type: string
+                                      value:
+                                        default: /
+                                        description: Value specifies the HTTP path.
+                                        maxLength: 1024
+                                        type: string
+                                    required:
+                                    - value
+                                    type: object
+                                  sourceCIDR:
+                                    description: SourceCIDR is the client IP Address
+                                      range to match on.
                                     properties:
                                       type:
                                         default: Exact
@@ -1745,6 +1862,11 @@ spec:
                                     - value
                                     type: object
                                 type: object
+                                x-kubernetes-validations:
+                                - message: at least one of headers, methods, path
+                                    or sourceCIDR must be specified
+                                  rule: has(self.headers) || has(self.methods) ||
+                                    has(self.path) || has(self.sourceCIDR)
                               maxItems: 8
                               type: array
                             cost:
@@ -2771,8 +2893,6 @@ spec:
             - message: this policy can only have a targetRef.kind of Gateway/HTTPRoute/GRPCRoute/TCPRoute/UDPRoute/TLSRoute
               rule: 'has(self.targetRef) ? self.targetRef.kind in [''Gateway'', ''HTTPRoute'',
                 ''GRPCRoute'', ''UDPRoute'', ''TCPRoute'', ''TLSRoute''] : true'
-            - message: this policy does not yet support the sectionName field
-              rule: 'has(self.targetRef) ? !has(self.targetRef.sectionName) : true'
             - message: this policy can only have a targetRefs[*].group of gateway.networking.k8s.io
               rule: 'has(self.targetRefs) ? self.targetRefs.all(ref, ref.group ==
                 ''gateway.networking.k8s.io'') : true '
@@ -2780,9 +2900,6 @@ spec:
               rule: 'has(self.targetRefs) ? self.targetRefs.all(ref, ref.kind in [''Gateway'',
                 ''HTTPRoute'', ''GRPCRoute'', ''UDPRoute'', ''TCPRoute'', ''TLSRoute''])
                 : true '
-            - message: this policy does not yet support the sectionName field
-              rule: 'has(self.targetRefs) ? self.targetRefs.all(ref, !has(ref.sectionName))
-                : true'
             - message: either compression or compressor can be set, not both
               rule: '!has(self.compression) || !has(self.compressor)'
             - message: predictivePercent in preconnect policy only works with RoundRobin
@@ -3620,12 +3737,13 @@ spec:
                       PreserveXRequestID configures Envoy to keep the X-Request-ID header if passed for a request that is edge
                       (Edge request is the request from external clients to front Envoy) and not reset it, which is the current Envoy behaviour.
                       Defaults to false and cannot be combined with RequestID.
-                      Deprecated: use RequestID=Preserve instead
+                      Deprecated: use RequestID=PreserveOrGenerate instead
                     type: boolean
                   requestID:
                     description: |-
                       RequestID configures Envoy's behavior for handling the `X-Request-ID` header.
-                      Defaults to `Generate` and builds the `X-Request-ID` for every request and ignores pre-existing values from the edge.
+                      When omitted default behavior is `Generate` which builds the `X-Request-ID` for every request
+                       and ignores pre-existing values from the edge.
                       (An "edge request" refers to a request from an external client to the Envoy entrypoint.)
                     enum:
                     - PreserveOrGenerate