You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@apisix.apache.org by al...@apache.org on 2026/05/18 01:01:29 UTC

(apisix-ingress-controller) branch master updated: feat: support upstream health checks in BackendTrafficPolicy (#2763)

This is an automated email from the ASF dual-hosted git repository.

AlinsRan pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix-ingress-controller.git


The following commit(s) were added to refs/heads/master by this push:
     new e461eb2e feat: support upstream health checks in BackendTrafficPolicy (#2763)
e461eb2e is described below

commit e461eb2e330c44d831b96c59d2c4093653a2b5b8
Author: AlinsRan <al...@apache.org>
AuthorDate: Mon May 18 09:01:24 2026 +0800

    feat: support upstream health checks in BackendTrafficPolicy (#2763)
---
 api/v1alpha1/backendtrafficpolicy_types.go         | 140 ++++++++++
 api/v1alpha1/zz_generated.deepcopy.go              | 165 +++++++++++
 .../apisix.apache.org_backendtrafficpolicies.yaml  | 175 ++++++++++++
 docs/en/latest/reference/api-reference.md          | 130 +++++++++
 internal/adc/translator/apisixconsumer_test.go     |   2 +-
 internal/adc/translator/httproute_test.go          | 311 +++++++++++++++++++++
 internal/adc/translator/policies.go                |  89 ++++++
 test/e2e/crds/v1alpha1/backendtrafficpolicy.go     | 216 ++++++++++++--
 8 files changed, 1207 insertions(+), 21 deletions(-)

diff --git a/api/v1alpha1/backendtrafficpolicy_types.go b/api/v1alpha1/backendtrafficpolicy_types.go
index 44d5239d..826ec854 100644
--- a/api/v1alpha1/backendtrafficpolicy_types.go
+++ b/api/v1alpha1/backendtrafficpolicy_types.go
@@ -74,6 +74,13 @@ type BackendTrafficPolicySpec struct {
 	// UpstreamHost specifies the host of the Upstream request. Used only if
 	// passHost is set to `rewrite`.
 	Host Hostname `json:"upstreamHost,omitempty" yaml:"upstreamHost,omitempty"`
+
+	// HealthCheck defines active and passive health check configuration for
+	// the upstream backends. When configured, APISIX will probe backends
+	// (active) or monitor live traffic (passive) to detect and bypass
+	// unhealthy nodes.
+	// +optional
+	HealthCheck *HealthCheck `json:"healthCheck,omitempty" yaml:"healthCheck,omitempty"`
 }
 
 // LoadBalancer describes the load balancing parameters.
@@ -125,6 +132,139 @@ type BackendTrafficPolicyList struct {
 	Items           []BackendTrafficPolicy `json:"items"`
 }
 
+// HealthCheck defines the active and passive health check configuration for upstream nodes.
+type HealthCheck struct {
+	// Active health checks proactively send requests to upstream nodes to determine their availability.
+	// +kubebuilder:validation:Required
+	Active *ActiveHealthCheck `json:"active" yaml:"active"`
+	// Passive health checks evaluate upstream health based on observed traffic (timeouts, errors).
+	// +kubebuilder:validation:Optional
+	Passive *PassiveHealthCheck `json:"passive,omitempty" yaml:"passive,omitempty"`
+}
+
+// ActiveHealthCheck defines the active upstream health check configuration.
+type ActiveHealthCheck struct {
+	// Type is the health check type. Can be `http`, `https`, or `tcp`.
+	// +kubebuilder:validation:Enum=http;https;tcp;
+	// +kubebuilder:default=http
+	// +optional
+	Type string `json:"type,omitempty" yaml:"type,omitempty"`
+
+	// Timeout sets health check timeout.
+	// +optional
+	Timeout metav1.Duration `json:"timeout,omitempty" yaml:"timeout,omitempty"`
+
+	// Concurrency sets the number of targets to be checked at the same time.
+	// +kubebuilder:validation:Minimum=0
+	// +optional
+	Concurrency int `json:"concurrency,omitempty" yaml:"concurrency,omitempty"`
+
+	// Host sets the upstream host used in the health check request.
+	// +optional
+	Host string `json:"host,omitempty" yaml:"host,omitempty"`
+
+	// Port sets the port on the upstream node to probe.
+	// +kubebuilder:validation:Minimum=1
+	// +kubebuilder:validation:Maximum=65535
+	// +optional
+	Port int32 `json:"port,omitempty" yaml:"port,omitempty"`
+
+	// HTTPPath sets the HTTP path for the probe request.
+	// +optional
+	HTTPPath string `json:"httpPath,omitempty" yaml:"httpPath,omitempty"`
+
+	// StrictTLS controls whether TLS certificate validation is enforced.
+	// +optional
+	StrictTLS *bool `json:"strictTLS,omitempty" yaml:"strictTLS,omitempty"`
+
+	// RequestHeaders sets additional HTTP request headers for the probe.
+	// +optional
+	RequestHeaders []string `json:"requestHeaders,omitempty" yaml:"requestHeaders,omitempty"`
+
+	// Healthy configures the thresholds for marking a node healthy.
+	// +optional
+	Healthy *ActiveHealthCheckHealthy `json:"healthy,omitempty" yaml:"healthy,omitempty"`
+
+	// Unhealthy configures the thresholds for marking a node unhealthy.
+	// +optional
+	Unhealthy *ActiveHealthCheckUnhealthy `json:"unhealthy,omitempty" yaml:"unhealthy,omitempty"`
+}
+
+// PassiveHealthCheck defines passive health check configuration based on observed traffic.
+type PassiveHealthCheck struct {
+	// Type is the passive health check type. Can be `http`, `https`, or `tcp`.
+	// +kubebuilder:validation:Enum=http;https;tcp;
+	// +kubebuilder:default=http
+	// +optional
+	Type string `json:"type,omitempty" yaml:"type,omitempty"`
+
+	// Healthy defines conditions under which a node is considered healthy.
+	// +optional
+	Healthy *PassiveHealthCheckHealthy `json:"healthy,omitempty" yaml:"healthy,omitempty"`
+
+	// Unhealthy defines conditions under which a node is considered unhealthy.
+	// +optional
+	Unhealthy *PassiveHealthCheckUnhealthy `json:"unhealthy,omitempty" yaml:"unhealthy,omitempty"`
+}
+
+// ActiveHealthCheckHealthy defines thresholds for actively marking an upstream node healthy.
+type ActiveHealthCheckHealthy struct {
+	PassiveHealthCheckHealthy `json:",inline" yaml:",inline"`
+
+	// Interval defines the time between health check probes.
+	// Minimum is 1s.
+	Interval metav1.Duration `json:"interval,omitempty" yaml:"interval,omitempty"`
+}
+
+// ActiveHealthCheckUnhealthy defines thresholds for actively marking an upstream node unhealthy.
+type ActiveHealthCheckUnhealthy struct {
+	PassiveHealthCheckUnhealthy `json:",inline" yaml:",inline"`
+
+	// Interval defines the time between health check probes.
+	// Minimum is 1s.
+	Interval metav1.Duration `json:"interval,omitempty" yaml:"interval,omitempty"`
+}
+
+// PassiveHealthCheckHealthy defines conditions for passively marking a node healthy.
+type PassiveHealthCheckHealthy struct {
+	// HTTPCodes is the list of HTTP status codes considered healthy.
+	// +kubebuilder:validation:MinItems=1
+	// +optional
+	HTTPCodes []int `json:"httpCodes,omitempty" yaml:"httpCodes,omitempty"`
+
+	// Successes is the number of consecutive successful responses required to mark a node healthy.
+	// +kubebuilder:validation:Minimum=0
+	// +kubebuilder:validation:Maximum=254
+	// +optional
+	Successes int `json:"successes,omitempty" yaml:"successes,omitempty"`
+}
+
+// PassiveHealthCheckUnhealthy defines conditions for passively marking a node unhealthy.
+type PassiveHealthCheckUnhealthy struct {
+	// HTTPCodes is the list of HTTP status codes considered unhealthy.
+	// +kubebuilder:validation:MinItems=1
+	// +optional
+	HTTPCodes []int `json:"httpCodes,omitempty" yaml:"httpCodes,omitempty"`
+
+	// HTTPFailures is the number of HTTP failures to mark a node unhealthy.
+	// +kubebuilder:validation:Minimum=0
+	// +kubebuilder:validation:Maximum=254
+	// +optional
+	HTTPFailures int `json:"httpFailures,omitempty" yaml:"httpFailures,omitempty"`
+
+	// TCPFailures is the number of TCP failures to mark a node unhealthy.
+	// +kubebuilder:validation:Minimum=0
+	// +kubebuilder:validation:Maximum=254
+	// +optional
+	TCPFailures int `json:"tcpFailures,omitempty" yaml:"tcpFailures,omitempty"`
+
+	// Timeouts is the number of timeouts to mark a node unhealthy.
+	// +kubebuilder:validation:Minimum=1
+	// +kubebuilder:validation:Maximum=254
+	// +optional
+	Timeouts int `json:"timeouts,omitempty" yaml:"timeouts,omitempty"`
+}
+
 func init() {
 	SchemeBuilder.Register(&BackendTrafficPolicy{}, &BackendTrafficPolicyList{})
 }
diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go
index cc03a549..9e1c02ff 100644
--- a/api/v1alpha1/zz_generated.deepcopy.go
+++ b/api/v1alpha1/zz_generated.deepcopy.go
@@ -26,6 +26,76 @@ import (
 	"sigs.k8s.io/gateway-api/apis/v1alpha2"
 )
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ActiveHealthCheck) DeepCopyInto(out *ActiveHealthCheck) {
+	*out = *in
+	out.Timeout = in.Timeout
+	if in.StrictTLS != nil {
+		in, out := &in.StrictTLS, &out.StrictTLS
+		*out = new(bool)
+		**out = **in
+	}
+	if in.RequestHeaders != nil {
+		in, out := &in.RequestHeaders, &out.RequestHeaders
+		*out = make([]string, len(*in))
+		copy(*out, *in)
+	}
+	if in.Healthy != nil {
+		in, out := &in.Healthy, &out.Healthy
+		*out = new(ActiveHealthCheckHealthy)
+		(*in).DeepCopyInto(*out)
+	}
+	if in.Unhealthy != nil {
+		in, out := &in.Unhealthy, &out.Unhealthy
+		*out = new(ActiveHealthCheckUnhealthy)
+		(*in).DeepCopyInto(*out)
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ActiveHealthCheck.
+func (in *ActiveHealthCheck) DeepCopy() *ActiveHealthCheck {
+	if in == nil {
+		return nil
+	}
+	out := new(ActiveHealthCheck)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ActiveHealthCheckHealthy) DeepCopyInto(out *ActiveHealthCheckHealthy) {
+	*out = *in
+	in.PassiveHealthCheckHealthy.DeepCopyInto(&out.PassiveHealthCheckHealthy)
+	out.Interval = in.Interval
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ActiveHealthCheckHealthy.
+func (in *ActiveHealthCheckHealthy) DeepCopy() *ActiveHealthCheckHealthy {
+	if in == nil {
+		return nil
+	}
+	out := new(ActiveHealthCheckHealthy)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ActiveHealthCheckUnhealthy) DeepCopyInto(out *ActiveHealthCheckUnhealthy) {
+	*out = *in
+	in.PassiveHealthCheckUnhealthy.DeepCopyInto(&out.PassiveHealthCheckUnhealthy)
+	out.Interval = in.Interval
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ActiveHealthCheckUnhealthy.
+func (in *ActiveHealthCheckUnhealthy) DeepCopy() *ActiveHealthCheckUnhealthy {
+	if in == nil {
+		return nil
+	}
+	out := new(ActiveHealthCheckUnhealthy)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *AdminKeyAuth) DeepCopyInto(out *AdminKeyAuth) {
 	*out = *in
@@ -171,6 +241,11 @@ func (in *BackendTrafficPolicySpec) DeepCopyInto(out *BackendTrafficPolicySpec)
 		*out = new(Timeout)
 		**out = **in
 	}
+	if in.HealthCheck != nil {
+		in, out := &in.HealthCheck, &out.HealthCheck
+		*out = new(HealthCheck)
+		(*in).DeepCopyInto(*out)
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackendTrafficPolicySpec.
@@ -616,6 +691,31 @@ func (in *HTTPRoutePolicySpec) DeepCopy() *HTTPRoutePolicySpec {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *HealthCheck) DeepCopyInto(out *HealthCheck) {
+	*out = *in
+	if in.Active != nil {
+		in, out := &in.Active, &out.Active
+		*out = new(ActiveHealthCheck)
+		(*in).DeepCopyInto(*out)
+	}
+	if in.Passive != nil {
+		in, out := &in.Passive, &out.Passive
+		*out = new(PassiveHealthCheck)
+		(*in).DeepCopyInto(*out)
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HealthCheck.
+func (in *HealthCheck) DeepCopy() *HealthCheck {
+	if in == nil {
+		return nil
+	}
+	out := new(HealthCheck)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *LoadBalancer) DeepCopyInto(out *LoadBalancer) {
 	*out = *in
@@ -631,6 +731,71 @@ func (in *LoadBalancer) DeepCopy() *LoadBalancer {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *PassiveHealthCheck) DeepCopyInto(out *PassiveHealthCheck) {
+	*out = *in
+	if in.Healthy != nil {
+		in, out := &in.Healthy, &out.Healthy
+		*out = new(PassiveHealthCheckHealthy)
+		(*in).DeepCopyInto(*out)
+	}
+	if in.Unhealthy != nil {
+		in, out := &in.Unhealthy, &out.Unhealthy
+		*out = new(PassiveHealthCheckUnhealthy)
+		(*in).DeepCopyInto(*out)
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PassiveHealthCheck.
+func (in *PassiveHealthCheck) DeepCopy() *PassiveHealthCheck {
+	if in == nil {
+		return nil
+	}
+	out := new(PassiveHealthCheck)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *PassiveHealthCheckHealthy) DeepCopyInto(out *PassiveHealthCheckHealthy) {
+	*out = *in
+	if in.HTTPCodes != nil {
+		in, out := &in.HTTPCodes, &out.HTTPCodes
+		*out = make([]int, len(*in))
+		copy(*out, *in)
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PassiveHealthCheckHealthy.
+func (in *PassiveHealthCheckHealthy) DeepCopy() *PassiveHealthCheckHealthy {
+	if in == nil {
+		return nil
+	}
+	out := new(PassiveHealthCheckHealthy)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *PassiveHealthCheckUnhealthy) DeepCopyInto(out *PassiveHealthCheckUnhealthy) {
+	*out = *in
+	if in.HTTPCodes != nil {
+		in, out := &in.HTTPCodes, &out.HTTPCodes
+		*out = make([]int, len(*in))
+		copy(*out, *in)
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PassiveHealthCheckUnhealthy.
+func (in *PassiveHealthCheckUnhealthy) DeepCopy() *PassiveHealthCheckUnhealthy {
+	if in == nil {
+		return nil
+	}
+	out := new(PassiveHealthCheckUnhealthy)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *Plugin) DeepCopyInto(out *Plugin) {
 	*out = *in
diff --git a/config/crd/bases/apisix.apache.org_backendtrafficpolicies.yaml b/config/crd/bases/apisix.apache.org_backendtrafficpolicies.yaml
index ed2cad68..c286a934 100644
--- a/config/crd/bases/apisix.apache.org_backendtrafficpolicies.yaml
+++ b/config/crd/bases/apisix.apache.org_backendtrafficpolicies.yaml
@@ -42,6 +42,181 @@ spec:
               BackendTrafficPolicySpec defines traffic handling policies applied to backend services,
               such as load balancing strategy, connection settings, and failover behavior.
             properties:
+              healthCheck:
+                description: |-
+                  HealthCheck defines active and passive health check configuration for
+                  the upstream backends. When configured, APISIX will probe backends
+                  (active) or monitor live traffic (passive) to detect and bypass
+                  unhealthy nodes.
+                properties:
+                  active:
+                    description: Active health checks proactively send requests to
+                      upstream nodes to determine their availability.
+                    properties:
+                      concurrency:
+                        description: Concurrency sets the number of targets to be
+                          checked at the same time.
+                        minimum: 0
+                        type: integer
+                      healthy:
+                        description: Healthy configures the thresholds for marking
+                          a node healthy.
+                        properties:
+                          httpCodes:
+                            description: HTTPCodes is the list of HTTP status codes
+                              considered healthy.
+                            items:
+                              type: integer
+                            minItems: 1
+                            type: array
+                          interval:
+                            description: |-
+                              Interval defines the time between health check probes.
+                              Minimum is 1s.
+                            type: string
+                          successes:
+                            description: Successes is the number of consecutive successful
+                              responses required to mark a node healthy.
+                            maximum: 254
+                            minimum: 0
+                            type: integer
+                        type: object
+                      host:
+                        description: Host sets the upstream host used in the health
+                          check request.
+                        type: string
+                      httpPath:
+                        description: HTTPPath sets the HTTP path for the probe request.
+                        type: string
+                      port:
+                        description: Port sets the port on the upstream node to probe.
+                        format: int32
+                        maximum: 65535
+                        minimum: 1
+                        type: integer
+                      requestHeaders:
+                        description: RequestHeaders sets additional HTTP request headers
+                          for the probe.
+                        items:
+                          type: string
+                        type: array
+                      strictTLS:
+                        description: StrictTLS controls whether TLS certificate validation
+                          is enforced.
+                        type: boolean
+                      timeout:
+                        description: Timeout sets health check timeout.
+                        type: string
+                      type:
+                        default: http
+                        description: Type is the health check type. Can be `http`,
+                          `https`, or `tcp`.
+                        enum:
+                        - http
+                        - https
+                        - tcp
+                        type: string
+                      unhealthy:
+                        description: Unhealthy configures the thresholds for marking
+                          a node unhealthy.
+                        properties:
+                          httpCodes:
+                            description: HTTPCodes is the list of HTTP status codes
+                              considered unhealthy.
+                            items:
+                              type: integer
+                            minItems: 1
+                            type: array
+                          httpFailures:
+                            description: HTTPFailures is the number of HTTP failures
+                              to mark a node unhealthy.
+                            maximum: 254
+                            minimum: 0
+                            type: integer
+                          interval:
+                            description: |-
+                              Interval defines the time between health check probes.
+                              Minimum is 1s.
+                            type: string
+                          tcpFailures:
+                            description: TCPFailures is the number of TCP failures
+                              to mark a node unhealthy.
+                            maximum: 254
+                            minimum: 0
+                            type: integer
+                          timeouts:
+                            description: Timeouts is the number of timeouts to mark
+                              a node unhealthy.
+                            maximum: 254
+                            minimum: 1
+                            type: integer
+                        type: object
+                    type: object
+                  passive:
+                    description: Passive health checks evaluate upstream health based
+                      on observed traffic (timeouts, errors).
+                    properties:
+                      healthy:
+                        description: Healthy defines conditions under which a node
+                          is considered healthy.
+                        properties:
+                          httpCodes:
+                            description: HTTPCodes is the list of HTTP status codes
+                              considered healthy.
+                            items:
+                              type: integer
+                            minItems: 1
+                            type: array
+                          successes:
+                            description: Successes is the number of consecutive successful
+                              responses required to mark a node healthy.
+                            maximum: 254
+                            minimum: 0
+                            type: integer
+                        type: object
+                      type:
+                        default: http
+                        description: Type is the passive health check type. Can be
+                          `http`, `https`, or `tcp`.
+                        enum:
+                        - http
+                        - https
+                        - tcp
+                        type: string
+                      unhealthy:
+                        description: Unhealthy defines conditions under which a node
+                          is considered unhealthy.
+                        properties:
+                          httpCodes:
+                            description: HTTPCodes is the list of HTTP status codes
+                              considered unhealthy.
+                            items:
+                              type: integer
+                            minItems: 1
+                            type: array
+                          httpFailures:
+                            description: HTTPFailures is the number of HTTP failures
+                              to mark a node unhealthy.
+                            maximum: 254
+                            minimum: 0
+                            type: integer
+                          tcpFailures:
+                            description: TCPFailures is the number of TCP failures
+                              to mark a node unhealthy.
+                            maximum: 254
+                            minimum: 0
+                            type: integer
+                          timeouts:
+                            description: Timeouts is the number of timeouts to mark
+                              a node unhealthy.
+                            maximum: 254
+                            minimum: 1
+                            type: integer
+                        type: object
+                    type: object
+                required:
+                - active
+                type: object
               loadbalancer:
                 description: |-
                   LoadBalancer represents the load balancer configuration for Kubernetes Service.
diff --git a/docs/en/latest/reference/api-reference.md b/docs/en/latest/reference/api-reference.md
index 11357fb3..3d8987ac 100644
--- a/docs/en/latest/reference/api-reference.md
+++ b/docs/en/latest/reference/api-reference.md
@@ -103,6 +103,66 @@ PluginConfig defines plugin configuration.
 ### Types
 
 This section describes the types used by the CRDs.
+#### ActiveHealthCheck
+
+
+ActiveHealthCheck defines the active upstream health check configuration.
+
+
+
+| Field | Description |
+| --- | --- |
+| `type` _string_ | Type is the health check type. Can be `http`, `https`, or `tcp`. |
+| `timeout` _[Duration](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#duration-v1-meta)_ | Timeout sets health check timeout. |
+| `concurrency` _integer_ | Concurrency sets the number of targets to be checked at the same time. |
+| `host` _string_ | Host sets the upstream host used in the health check request. |
+| `port` _integer_ | Port sets the port on the upstream node to probe. |
+| `httpPath` _string_ | HTTPPath sets the HTTP path for the probe request. |
+| `strictTLS` _boolean_ | StrictTLS controls whether TLS certificate validation is enforced. |
+| `requestHeaders` _string array_ | RequestHeaders sets additional HTTP request headers for the probe. |
+| `healthy` _[ActiveHealthCheckHealthy](#activehealthcheckhealthy)_ | Healthy configures the thresholds for marking a node healthy. |
+| `unhealthy` _[ActiveHealthCheckUnhealthy](#activehealthcheckunhealthy)_ | Unhealthy configures the thresholds for marking a node unhealthy. |
+
+
+_Appears in:_
+- [HealthCheck](#healthcheck)
+
+#### ActiveHealthCheckHealthy
+
+
+ActiveHealthCheckHealthy defines thresholds for actively marking an upstream node healthy.
+
+
+
+| Field | Description |
+| --- | --- |
+| `httpCodes` _integer array_ | HTTPCodes is the list of HTTP status codes considered healthy. |
+| `successes` _integer_ | Successes is the number of consecutive successful responses required to mark a node healthy. |
+| `interval` _[Duration](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#duration-v1-meta)_ | Interval defines the time between health check probes. Minimum is 1s. |
+
+
+_Appears in:_
+- [ActiveHealthCheck](#activehealthcheck)
+
+#### ActiveHealthCheckUnhealthy
+
+
+ActiveHealthCheckUnhealthy defines thresholds for actively marking an upstream node unhealthy.
+
+
+
+| Field | Description |
+| --- | --- |
+| `httpCodes` _integer array_ | HTTPCodes is the list of HTTP status codes considered unhealthy. |
+| `httpFailures` _integer_ | HTTPFailures is the number of HTTP failures to mark a node unhealthy. |
+| `tcpFailures` _integer_ | TCPFailures is the number of TCP failures to mark a node unhealthy. |
+| `timeouts` _integer_ | Timeouts is the number of timeouts to mark a node unhealthy. |
+| `interval` _[Duration](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#duration-v1-meta)_ | Interval defines the time between health check probes. Minimum is 1s. |
+
+
+_Appears in:_
+- [ActiveHealthCheck](#activehealthcheck)
+
 #### AdminKeyAuth
 
 
@@ -180,6 +240,7 @@ _Appears in:_
 | `timeout` _[Timeout](#timeout)_ | Timeout sets the read, send, and connect timeouts to the upstream. |
 | `passHost` _string_ | PassHost configures how the host header should be determined when a request is forwarded to the upstream. Default is `pass`. Can be `pass`, `node` or `rewrite`:<br /> • `pass`: preserve the original Host header<br /> • `node`: use the upstream node’s host<br /> • `rewrite`: set to a custom host via `upstreamHost` |
 | `upstreamHost` _[Hostname](#hostname)_ | UpstreamHost specifies the host of the Upstream request. Used only if passHost is set to `rewrite`. |
+| `healthCheck` _[HealthCheck](#healthcheck)_ | HealthCheck defines active and passive health check configuration for the upstream backends. When configured, APISIX will probe backends (active) or monitor live traffic (passive) to detect and bypass unhealthy nodes. |
 
 
 _Appears in:_
@@ -344,6 +405,22 @@ HTTPRoutePolicySpec defines the desired state of HTTPRoutePolicy.
 _Appears in:_
 - [HTTPRoutePolicy](#httproutepolicy)
 
+#### HealthCheck
+
+
+HealthCheck defines the active and passive health check configuration for upstream nodes.
+
+
+
+| Field | Description |
+| --- | --- |
+| `active` _[ActiveHealthCheck](#activehealthcheck)_ | Active health checks proactively send requests to upstream nodes to determine their availability. |
+| `passive` _[PassiveHealthCheck](#passivehealthcheck)_ | Passive health checks evaluate upstream health based on observed traffic (timeouts, errors). |
+
+
+_Appears in:_
+- [BackendTrafficPolicySpec](#backendtrafficpolicyspec)
+
 #### Hostname
 _Base type:_ `string`
 
@@ -373,6 +450,59 @@ LoadBalancer describes the load balancing parameters.
 _Appears in:_
 - [BackendTrafficPolicySpec](#backendtrafficpolicyspec)
 
+#### PassiveHealthCheck
+
+
+PassiveHealthCheck defines passive health check configuration based on observed traffic.
+
+
+
+| Field | Description |
+| --- | --- |
+| `type` _string_ | Type is the passive health check type. Can be `http`, `https`, or `tcp`. |
+| `healthy` _[PassiveHealthCheckHealthy](#passivehealthcheckhealthy)_ | Healthy defines conditions under which a node is considered healthy. |
+| `unhealthy` _[PassiveHealthCheckUnhealthy](#passivehealthcheckunhealthy)_ | Unhealthy defines conditions under which a node is considered unhealthy. |
+
+
+_Appears in:_
+- [HealthCheck](#healthcheck)
+
+#### PassiveHealthCheckHealthy
+
+
+PassiveHealthCheckHealthy defines conditions for passively marking a node healthy.
+
+
+
+| Field | Description |
+| --- | --- |
+| `httpCodes` _integer array_ | HTTPCodes is the list of HTTP status codes considered healthy. |
+| `successes` _integer_ | Successes is the number of consecutive successful responses required to mark a node healthy. |
+
+
+_Appears in:_
+- [ActiveHealthCheckHealthy](#activehealthcheckhealthy)
+- [PassiveHealthCheck](#passivehealthcheck)
+
+#### PassiveHealthCheckUnhealthy
+
+
+PassiveHealthCheckUnhealthy defines conditions for passively marking a node unhealthy.
+
+
+
+| Field | Description |
+| --- | --- |
+| `httpCodes` _integer array_ | HTTPCodes is the list of HTTP status codes considered unhealthy. |
+| `httpFailures` _integer_ | HTTPFailures is the number of HTTP failures to mark a node unhealthy. |
+| `tcpFailures` _integer_ | TCPFailures is the number of TCP failures to mark a node unhealthy. |
+| `timeouts` _integer_ | Timeouts is the number of timeouts to mark a node unhealthy. |
+
+
+_Appears in:_
+- [ActiveHealthCheckUnhealthy](#activehealthcheckunhealthy)
+- [PassiveHealthCheck](#passivehealthcheck)
+
 #### Plugin
 
 
diff --git a/internal/adc/translator/apisixconsumer_test.go b/internal/adc/translator/apisixconsumer_test.go
index 7dbe131c..fe56b1ec 100644
--- a/internal/adc/translator/apisixconsumer_test.go
+++ b/internal/adc/translator/apisixconsumer_test.go
@@ -49,7 +49,7 @@ func TestTranslateApisixConsumer_UsesMetadataLabelsWithoutOverwritingControllerL
 			},
 		},
 		Spec: apiv2.ApisixConsumerSpec{
-			AuthParameter: apiv2.ApisixConsumerAuthParameter{
+			AuthParameter: &apiv2.ApisixConsumerAuthParameter{
 				BasicAuth: &apiv2.ApisixConsumerBasicAuth{
 					Value: &apiv2.ApisixConsumerBasicAuthValue{
 						Username: "demo",
diff --git a/internal/adc/translator/httproute_test.go b/internal/adc/translator/httproute_test.go
new file mode 100644
index 00000000..7b11e129
--- /dev/null
+++ b/internal/adc/translator/httproute_test.go
@@ -0,0 +1,311 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package translator
+
+import (
+	"context"
+	"testing"
+	"time"
+
+	"github.com/go-logr/logr"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	corev1 "k8s.io/api/core/v1"
+	discoveryv1 "k8s.io/api/discovery/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/types"
+	"k8s.io/utils/ptr"
+	gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
+	gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
+
+	adctypes "github.com/apache/apisix-ingress-controller/api/adc"
+	"github.com/apache/apisix-ingress-controller/api/v1alpha1"
+	apiv2 "github.com/apache/apisix-ingress-controller/api/v2"
+	"github.com/apache/apisix-ingress-controller/internal/provider"
+	internaltypes "github.com/apache/apisix-ingress-controller/internal/types"
+)
+
+func TestTranslateHTTPRouteUpstreamScheme(t *testing.T) {
+	tests := []struct {
+		name         string
+		appProtocol  string
+		policyScheme string
+		wantScheme   string
+	}{
+		{
+			name:         "preserves backend traffic policy scheme",
+			appProtocol:  internaltypes.AppProtocolHTTP,
+			policyScheme: apiv2.SchemeHTTPS,
+			wantScheme:   apiv2.SchemeHTTPS,
+		},
+		{
+			name:        "falls back to app protocol when scheme is unset",
+			appProtocol: internaltypes.AppProtocolWSS,
+			wantScheme:  apiv2.SchemeHTTPS,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			translator := NewTranslator(logr.Discard())
+			tctx := provider.NewDefaultTranslateContext(context.Background())
+
+			const (
+				namespace   = "default"
+				serviceName = "backend"
+				portName    = "web"
+				portNumber  = int32(8443)
+			)
+
+			serviceKey := types.NamespacedName{Namespace: namespace, Name: serviceName}
+			tctx.Services[serviceKey] = &corev1.Service{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      serviceName,
+					Namespace: namespace,
+				},
+				Spec: corev1.ServiceSpec{
+					Ports: []corev1.ServicePort{{
+						Name:        portName,
+						Port:        portNumber,
+						AppProtocol: ptr.To(tt.appProtocol),
+					}},
+				},
+			}
+			tctx.EndpointSlices[serviceKey] = []discoveryv1.EndpointSlice{{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "backend-1",
+					Namespace: namespace,
+				},
+				Ports: []discoveryv1.EndpointPort{{
+					Name: ptr.To(portName),
+					Port: ptr.To(portNumber),
+				}},
+				Endpoints: []discoveryv1.Endpoint{{
+					Addresses: []string{"10.0.0.1"},
+					Conditions: discoveryv1.EndpointConditions{
+						Ready: ptr.To(true),
+					},
+				}},
+			}}
+
+			if tt.policyScheme != "" {
+				tctx.BackendTrafficPolicies[serviceKey] = &v1alpha1.BackendTrafficPolicy{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "backend-policy",
+						Namespace: namespace,
+					},
+					Spec: v1alpha1.BackendTrafficPolicySpec{
+						TargetRefs: []v1alpha1.BackendPolicyTargetReferenceWithSectionName{{
+							LocalPolicyTargetReference: gatewayv1alpha2.LocalPolicyTargetReference{
+								Name: gatewayv1alpha2.ObjectName(serviceName),
+								Kind: gatewayv1alpha2.Kind(internaltypes.KindService),
+							},
+						}},
+						Scheme: tt.policyScheme,
+					},
+				}
+			}
+
+			route := &gatewayv1.HTTPRoute{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "demo",
+					Namespace: namespace,
+				},
+				Spec: gatewayv1.HTTPRouteSpec{
+					Rules: []gatewayv1.HTTPRouteRule{{
+						BackendRefs: []gatewayv1.HTTPBackendRef{{
+							BackendRef: gatewayv1.BackendRef{
+								BackendObjectReference: gatewayv1.BackendObjectReference{
+									Name: gatewayv1.ObjectName(serviceName),
+									Port: ptr.To(gatewayv1.PortNumber(portNumber)),
+								},
+							},
+						}},
+					}},
+				},
+			}
+
+			result, err := translator.TranslateHTTPRoute(tctx, route)
+			require.NoError(t, err)
+			require.Len(t, result.Services, 1)
+			require.NotNil(t, result.Services[0].Upstream)
+
+			assert.Equal(t, tt.wantScheme, result.Services[0].Upstream.Scheme)
+			assert.Equal(t, "10.0.0.1", result.Services[0].Upstream.Nodes[0].Host)
+		})
+	}
+}
+
+func TestAttachBackendTrafficPolicyHealthCheck(t *testing.T) {
+	trueVal := true
+	falseVal := false
+
+	tests := []struct {
+		name       string
+		policy     *v1alpha1.BackendTrafficPolicy
+		wantChecks *adctypes.UpstreamHealthCheck
+	}{
+		{
+			name:       "nil health check produces no checks",
+			policy:     &v1alpha1.BackendTrafficPolicy{},
+			wantChecks: nil,
+		},
+		{
+			name: "active health check with all fields",
+			policy: &v1alpha1.BackendTrafficPolicy{
+				Spec: v1alpha1.BackendTrafficPolicySpec{
+					HealthCheck: &v1alpha1.HealthCheck{
+						Active: &v1alpha1.ActiveHealthCheck{
+							Type:           "http",
+							Timeout:        metav1.Duration{Duration: 3 * time.Second},
+							HTTPPath:       "/healthz",
+							Concurrency:    10,
+							Host:           "example.com",
+							Port:           8080,
+							StrictTLS:      &trueVal,
+							RequestHeaders: []string{"X-Custom: value"},
+							Healthy: &v1alpha1.ActiveHealthCheckHealthy{
+								Interval: metav1.Duration{Duration: 5 * time.Second},
+								PassiveHealthCheckHealthy: v1alpha1.PassiveHealthCheckHealthy{
+									HTTPCodes: []int{200, 201},
+									Successes: 3,
+								},
+							},
+							Unhealthy: &v1alpha1.ActiveHealthCheckUnhealthy{
+								Interval: metav1.Duration{Duration: 2 * time.Second},
+								PassiveHealthCheckUnhealthy: v1alpha1.PassiveHealthCheckUnhealthy{
+									HTTPCodes:    []int{500, 503},
+									HTTPFailures: 5,
+									TCPFailures:  2,
+									Timeouts:     3,
+								},
+							},
+						},
+					},
+				},
+			},
+			wantChecks: &adctypes.UpstreamHealthCheck{
+				Active: &adctypes.UpstreamActiveHealthCheck{
+					Type:                   "http",
+					Timeout:                3,
+					HTTPPath:               "/healthz",
+					Concurrency:            10,
+					Host:                   "example.com",
+					Port:                   8080,
+					HTTPSVerifyCertificate: true,
+					HTTPRequestHeaders:     []string{"X-Custom: value"},
+					Healthy: adctypes.UpstreamActiveHealthCheckHealthy{
+						Interval: 5,
+						UpstreamPassiveHealthCheckHealthy: adctypes.UpstreamPassiveHealthCheckHealthy{
+							HTTPStatuses: []int{200, 201},
+							Successes:    3,
+						},
+					},
+					Unhealthy: adctypes.UpstreamActiveHealthCheckUnhealthy{
+						Interval: 2,
+						UpstreamPassiveHealthCheckUnhealthy: adctypes.UpstreamPassiveHealthCheckUnhealthy{
+							HTTPStatuses: []int{500, 503},
+							HTTPFailures: 5,
+							TCPFailures:  2,
+							Timeouts:     3,
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "strictTLS false disables certificate verification",
+			policy: &v1alpha1.BackendTrafficPolicy{
+				Spec: v1alpha1.BackendTrafficPolicySpec{
+					HealthCheck: &v1alpha1.HealthCheck{
+						Active: &v1alpha1.ActiveHealthCheck{
+							StrictTLS: &falseVal,
+							Healthy: &v1alpha1.ActiveHealthCheckHealthy{
+								Interval: metav1.Duration{Duration: 1 * time.Second},
+							},
+						},
+					},
+				},
+			},
+			wantChecks: &adctypes.UpstreamHealthCheck{
+				Active: &adctypes.UpstreamActiveHealthCheck{
+					Type:                   "http",
+					HTTPSVerifyCertificate: false,
+					Healthy: adctypes.UpstreamActiveHealthCheckHealthy{
+						Interval: 1,
+					},
+				},
+			},
+		},
+		{
+			name: "active and passive health checks together",
+			policy: &v1alpha1.BackendTrafficPolicy{
+				Spec: v1alpha1.BackendTrafficPolicySpec{
+					HealthCheck: &v1alpha1.HealthCheck{
+						Active: &v1alpha1.ActiveHealthCheck{
+							Type: "tcp",
+							Healthy: &v1alpha1.ActiveHealthCheckHealthy{
+								Interval: metav1.Duration{Duration: 1 * time.Second},
+							},
+						},
+						Passive: &v1alpha1.PassiveHealthCheck{
+							Type: "http",
+							Healthy: &v1alpha1.PassiveHealthCheckHealthy{
+								HTTPCodes: []int{200},
+								Successes: 2,
+							},
+							Unhealthy: &v1alpha1.PassiveHealthCheckUnhealthy{
+								HTTPCodes:    []int{500},
+								HTTPFailures: 3,
+							},
+						},
+					},
+				},
+			},
+			wantChecks: &adctypes.UpstreamHealthCheck{
+				Active: &adctypes.UpstreamActiveHealthCheck{
+					Type:                   "tcp",
+					HTTPSVerifyCertificate: true,
+					Healthy: adctypes.UpstreamActiveHealthCheckHealthy{
+						Interval: 1,
+					},
+				},
+				Passive: &adctypes.UpstreamPassiveHealthCheck{
+					Type: "http",
+					Healthy: adctypes.UpstreamPassiveHealthCheckHealthy{
+						HTTPStatuses: []int{200},
+						Successes:    2,
+					},
+					Unhealthy: adctypes.UpstreamPassiveHealthCheckUnhealthy{
+						HTTPStatuses: []int{500},
+						HTTPFailures: 3,
+					},
+				},
+			},
+		},
+	}
+
+	translator := &Translator{Log: logr.Discard()}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			ups := adctypes.NewDefaultUpstream()
+			translator.attachBackendTrafficPolicyToUpstream(tt.policy, ups)
+			assert.Equal(t, tt.wantChecks, ups.Checks)
+		})
+	}
+}
diff --git a/internal/adc/translator/policies.go b/internal/adc/translator/policies.go
index 41706964..726fbfac 100644
--- a/internal/adc/translator/policies.go
+++ b/internal/adc/translator/policies.go
@@ -24,6 +24,7 @@ import (
 
 	adctypes "github.com/apache/apisix-ingress-controller/api/adc"
 	"github.com/apache/apisix-ingress-controller/api/v1alpha1"
+	apiv2 "github.com/apache/apisix-ingress-controller/api/v2"
 )
 
 func convertBackendRef(namespace, name, kind string) gatewayv1.BackendRef {
@@ -79,4 +80,92 @@ func (t *Translator) attachBackendTrafficPolicyToUpstream(policy *v1alpha1.Backe
 		upstream.HashOn = policy.Spec.LoadBalancer.HashOn
 		upstream.Key = policy.Spec.LoadBalancer.Key
 	}
+	if policy.Spec.HealthCheck != nil {
+		upstream.Checks = translateBTPHealthCheck(policy.Spec.HealthCheck)
+	}
+}
+
+func translateBTPHealthCheck(hc *v1alpha1.HealthCheck) *adctypes.UpstreamHealthCheck {
+	if hc == nil || (hc.Active == nil && hc.Passive == nil) {
+		return nil
+	}
+	result := &adctypes.UpstreamHealthCheck{}
+	if hc.Active != nil {
+		result.Active = translateBTPActiveHealthCheck(hc.Active)
+	}
+	if hc.Passive != nil {
+		result.Passive = translateBTPPassiveHealthCheck(hc.Passive)
+	}
+	return result
+}
+
+func translateBTPActiveHealthCheck(config *v1alpha1.ActiveHealthCheck) *adctypes.UpstreamActiveHealthCheck {
+	t := config.Type
+	if t == "" {
+		t = "http"
+	}
+	active := &adctypes.UpstreamActiveHealthCheck{
+		Type:                   t,
+		Timeout:                int(config.Timeout.Seconds()),
+		Concurrency:            config.Concurrency,
+		Host:                   config.Host,
+		Port:                   config.Port,
+		HTTPPath:               config.HTTPPath,
+		HTTPSVerifyCertificate: config.StrictTLS == nil || *config.StrictTLS,
+		HTTPRequestHeaders:     config.RequestHeaders,
+	}
+	if config.Healthy != nil {
+		interval := config.Healthy.Interval.Duration
+		if interval < apiv2.ActiveHealthCheckMinInterval {
+			interval = apiv2.ActiveHealthCheckMinInterval
+		}
+		active.Healthy = adctypes.UpstreamActiveHealthCheckHealthy{
+			Interval: int(interval.Seconds()),
+			UpstreamPassiveHealthCheckHealthy: adctypes.UpstreamPassiveHealthCheckHealthy{
+				HTTPStatuses: config.Healthy.HTTPCodes,
+				Successes:    config.Healthy.Successes,
+			},
+		}
+	}
+	if config.Unhealthy != nil {
+		interval := config.Unhealthy.Interval.Duration
+		if interval < apiv2.ActiveHealthCheckMinInterval {
+			interval = apiv2.ActiveHealthCheckMinInterval
+		}
+		active.Unhealthy = adctypes.UpstreamActiveHealthCheckUnhealthy{
+			Interval: int(interval.Seconds()),
+			UpstreamPassiveHealthCheckUnhealthy: adctypes.UpstreamPassiveHealthCheckUnhealthy{
+				HTTPStatuses: config.Unhealthy.HTTPCodes,
+				HTTPFailures: config.Unhealthy.HTTPFailures,
+				TCPFailures:  config.Unhealthy.TCPFailures,
+				Timeouts:     config.Unhealthy.Timeouts,
+			},
+		}
+	}
+	return active
+}
+
+func translateBTPPassiveHealthCheck(config *v1alpha1.PassiveHealthCheck) *adctypes.UpstreamPassiveHealthCheck {
+	t := config.Type
+	if t == "" {
+		t = "http"
+	}
+	passive := &adctypes.UpstreamPassiveHealthCheck{
+		Type: t,
+	}
+	if config.Healthy != nil {
+		passive.Healthy = adctypes.UpstreamPassiveHealthCheckHealthy{
+			HTTPStatuses: config.Healthy.HTTPCodes,
+			Successes:    config.Healthy.Successes,
+		}
+	}
+	if config.Unhealthy != nil {
+		passive.Unhealthy = adctypes.UpstreamPassiveHealthCheckUnhealthy{
+			HTTPStatuses: config.Unhealthy.HTTPCodes,
+			HTTPFailures: config.Unhealthy.HTTPFailures,
+			TCPFailures:  config.Unhealthy.TCPFailures,
+			Timeouts:     config.Unhealthy.Timeouts,
+		}
+	}
+	return passive
 }
diff --git a/test/e2e/crds/v1alpha1/backendtrafficpolicy.go b/test/e2e/crds/v1alpha1/backendtrafficpolicy.go
index d26b6630..5704757e 100644
--- a/test/e2e/crds/v1alpha1/backendtrafficpolicy.go
+++ b/test/e2e/crds/v1alpha1/backendtrafficpolicy.go
@@ -18,6 +18,7 @@
 package v1alpha1
 
 import (
+	"context"
 	"fmt"
 	"time"
 
@@ -25,6 +26,7 @@ import (
 	. "github.com/onsi/gomega"
 	"k8s.io/apimachinery/pkg/types"
 
+	adctypes "github.com/apache/apisix-ingress-controller/api/adc"
 	"github.com/apache/apisix-ingress-controller/test/e2e/scaffold"
 )
 
@@ -57,6 +59,26 @@ spec:
     - name: httpbin-service-e2e-test
       port: 80
 `
+	var gatewayBeforeEach = func() {
+		By("create GatewayProxy")
+		err = s.CreateResourceFromString(s.GetGatewayProxySpec())
+		Expect(err).NotTo(HaveOccurred(), "creating GatewayProxy")
+		time.Sleep(5 * time.Second)
+
+		By("create GatewayClass")
+		err = s.CreateResourceFromString(s.GetGatewayClassYaml())
+		Expect(err).NotTo(HaveOccurred(), "creating GatewayClass")
+		time.Sleep(5 * time.Second)
+
+		By("create Gateway")
+		err = s.CreateResourceFromString(s.GetGatewayYaml())
+		Expect(err).NotTo(HaveOccurred(), "creating Gateway")
+		time.Sleep(5 * time.Second)
+
+		By("create HTTPRoute")
+		s.ApplyHTTPRoute(types.NamespacedName{Namespace: s.Namespace(), Name: "httpbin"}, fmt.Sprintf(defaultHTTPRoute, s.Namespace(), s.Namespace()))
+	}
+
 	Context("Rewrite Upstream Host", func() {
 		var createUpstreamHost = `
 apiVersion: apisix.apache.org/v1alpha1
@@ -86,26 +108,8 @@ spec:
   upstreamHost: httpbin.update.example.com
 `
 
-		BeforeEach(func() {
-			gatewayName := s.Namespace()
-			By("create GatewayProxy")
-			err = s.CreateResourceFromString(s.GetGatewayProxySpec())
-			Expect(err).NotTo(HaveOccurred(), "creating GatewayProxy")
-			time.Sleep(time.Second)
-
-			By("create GatewayClass")
-			err = s.CreateResourceFromString(s.GetGatewayClassYaml())
-			Expect(err).NotTo(HaveOccurred(), "creating GatewayClass")
-			time.Sleep(time.Second)
-
-			By("create Gateway")
-			err = s.CreateResourceFromString(s.GetGatewayYaml())
-			Expect(err).NotTo(HaveOccurred(), "creating Gateway")
-			time.Sleep(time.Second)
-
-			By("create HTTPRoute")
-			s.ApplyHTTPRoute(types.NamespacedName{Namespace: s.Namespace(), Name: "httpbin"}, fmt.Sprintf(defaultHTTPRoute, gatewayName, s.Namespace()))
-		})
+		BeforeEach(gatewayBeforeEach)
+
 		It("should rewrite upstream host", func() {
 			s.ResourceApplied("BackendTrafficPolicy", "httpbin", createUpstreamHost, 1)
 			s.RequestAssert(&scaffold.RequestAssert{
@@ -159,6 +163,178 @@ spec:
 			})
 		})
 	})
+
+	Context("Health Check", func() {
+		var policyWithActiveHealthCheck = `
+apiVersion: apisix.apache.org/v1alpha1
+kind: BackendTrafficPolicy
+metadata:
+  name: httpbin
+spec:
+  targetRefs:
+  - name: httpbin-service-e2e-test
+    kind: Service
+    group: ""
+  healthCheck:
+    active:
+      type: http
+      httpPath: /get
+      healthy:
+        httpCodes: [200]
+        interval: 1s
+      unhealthy:
+        httpCodes: [500]
+        httpFailures: 2
+        interval: 1s
+`
+
+		var policyWithActiveAndPassiveHealthCheck = `
+apiVersion: apisix.apache.org/v1alpha1
+kind: BackendTrafficPolicy
+metadata:
+  name: httpbin
+spec:
+  targetRefs:
+  - name: httpbin-service-e2e-test
+    kind: Service
+    group: ""
+  healthCheck:
+    active:
+      type: http
+      httpPath: /get
+      healthy:
+        httpCodes: [200]
+        interval: 1s
+      unhealthy:
+        httpCodes: [500]
+        httpFailures: 2
+        interval: 1s
+    passive:
+      type: http
+      healthy:
+        httpCodes: [200]
+      unhealthy:
+        httpCodes: [502, 503]
+        httpFailures: 3
+`
+
+		BeforeEach(gatewayBeforeEach)
+
+		It("should configure active health check on upstream", func() {
+			s.ResourceApplied("BackendTrafficPolicy", "httpbin", policyWithActiveHealthCheck, 1)
+
+			// Trigger some traffic so APISIX registers the upstream
+			s.RequestAssert(&scaffold.RequestAssert{
+				Method: "GET",
+				Path:   "/get",
+				Host:   "httpbin.org",
+				Checks: []scaffold.ResponseCheckFunc{
+					scaffold.WithExpectedStatus(200),
+				},
+			})
+			time.Sleep(2 * time.Second)
+
+			ups, err := s.DefaultDataplaneResource().Upstream().List(context.Background())
+			Expect(err).ToNot(HaveOccurred(), "listing upstreams")
+			Expect(ups).NotTo(BeEmpty(), "upstreams should not be empty")
+
+			var target *adctypes.Upstream
+			for _, u := range ups {
+				if u.Checks != nil {
+					target = u
+					break
+				}
+			}
+			Expect(target).NotTo(BeNil(), "upstream with health check should exist")
+			Expect(target.Checks.Active).NotTo(BeNil(), "active health check should be configured")
+			Expect(target.Checks.Active.HTTPPath).To(Equal("/get"), "active health check http path")
+			Expect(target.Checks.Active.Healthy.Interval).To(Equal(1), "active healthy interval")
+			Expect(target.Checks.Active.Healthy.HTTPStatuses).To(Equal([]int{200}), "active healthy http codes")
+			Expect(target.Checks.Active.Unhealthy.Interval).To(Equal(1), "active unhealthy interval")
+			Expect(target.Checks.Active.Unhealthy.HTTPFailures).To(Equal(2), "active unhealthy http failures")
+			Expect(target.Checks.Active.Unhealthy.HTTPStatuses).To(Equal([]int{500}), "active unhealthy http codes")
+			Expect(target.Checks.Passive).To(BeNil(), "passive health check should not be configured")
+		})
+
+		It("should configure active and passive health checks on upstream", func() {
+			s.ResourceApplied("BackendTrafficPolicy", "httpbin", policyWithActiveAndPassiveHealthCheck, 1)
+
+			// Trigger some traffic so APISIX registers the upstream
+			s.RequestAssert(&scaffold.RequestAssert{
+				Method: "GET",
+				Path:   "/get",
+				Host:   "httpbin.org",
+				Checks: []scaffold.ResponseCheckFunc{
+					scaffold.WithExpectedStatus(200),
+				},
+			})
+			time.Sleep(2 * time.Second)
+
+			ups, err := s.DefaultDataplaneResource().Upstream().List(context.Background())
+			Expect(err).ToNot(HaveOccurred(), "listing upstreams")
+			Expect(ups).NotTo(BeEmpty(), "upstreams should not be empty")
+
+			var target *adctypes.Upstream
+			for _, u := range ups {
+				if u.Checks != nil && u.Checks.Passive != nil {
+					target = u
+					break
+				}
+			}
+			Expect(target).NotTo(BeNil(), "upstream with active and passive health check should exist")
+
+			// Verify active health check
+			Expect(target.Checks.Active).NotTo(BeNil(), "active health check should be configured")
+			Expect(target.Checks.Active.HTTPPath).To(Equal("/get"), "active health check http path")
+			Expect(target.Checks.Active.Healthy.HTTPStatuses).To(Equal([]int{200}), "active healthy http codes")
+			Expect(target.Checks.Active.Unhealthy.HTTPFailures).To(Equal(2), "active unhealthy http failures")
+
+			// Verify passive health check
+			Expect(target.Checks.Passive.Healthy.HTTPStatuses).To(Equal([]int{200}), "passive healthy http codes")
+			Expect(target.Checks.Passive.Unhealthy.HTTPStatuses).To(Equal([]int{502, 503}), "passive unhealthy http codes")
+			Expect(target.Checks.Passive.Unhealthy.HTTPFailures).To(Equal(3), "passive unhealthy http failures")
+		})
+
+		It("should remove health check when policy is deleted", func() {
+			s.ResourceApplied("BackendTrafficPolicy", "httpbin", policyWithActiveHealthCheck, 1)
+
+			// Trigger traffic to establish upstream
+			s.RequestAssert(&scaffold.RequestAssert{
+				Method: "GET",
+				Path:   "/get",
+				Host:   "httpbin.org",
+				Checks: []scaffold.ResponseCheckFunc{
+					scaffold.WithExpectedStatus(200),
+				},
+			})
+			time.Sleep(2 * time.Second)
+
+			// Verify health check is present on the target upstream
+			ups, err := s.DefaultDataplaneResource().Upstream().List(context.Background())
+			Expect(err).ToNot(HaveOccurred())
+			hasHealthCheck := false
+			for _, u := range ups {
+				if u.Checks != nil {
+					hasHealthCheck = true
+					break
+				}
+			}
+			Expect(hasHealthCheck).To(BeTrue(), "upstream should have health check before policy deletion")
+
+			// Delete the policy
+			err = s.DeleteResourceFromString(policyWithActiveHealthCheck)
+			Expect(err).NotTo(HaveOccurred(), "deleting BackendTrafficPolicy")
+			time.Sleep(3 * time.Second)
+
+			// Verify health check is removed from the target upstream
+			ups, err = s.DefaultDataplaneResource().Upstream().List(context.Background())
+			Expect(err).ToNot(HaveOccurred())
+			Expect(ups).NotTo(BeEmpty(), "upstreams should still exist after policy deletion")
+			for _, u := range ups {
+				Expect(u.Checks).To(BeNil(), "upstream should not have health check after policy deletion")
+			}
+		})
+	})
 })
 
 var _ = Describe("Test BackendTrafficPolicy base on Ingress", Label("apisix.apache.org", "v1alpha1", "backendtrafficpolicy"), func() {