Session Persistence
Learn how to configure session persistence using NGINX Gateway Fabric.
In this guide, you’ll learn how to configure session persistence for your application. Session persistence ensures that multiple requests from the same client are consistently routed to the same backend Pod. This is useful when your application maintains in-memory state (for example, shopping carts or user sessions). NGINX Gateway Fabric supports cookie-based session persistence for both NGINX Open Source and NGINX Plus, configured using the sessionPersistence field directly on HTTPRoute and GRPCRoute resources.
In this guide, you will deploy two applications:
- An application configured with cookie-based session persistence.
- A regular application with default load-balancing.
These applications will showcase the benefits of session persistence for stateful workloads.
The NGINX directive discussed in this guide is sticky cookie.
ImportantSession Persistence is a Gateway API field from the experimental release channel and is subject to change.
DeprecatedThe experimentalsessionPersistencefield onHTTPRouteRuleandGRPCRouteRuleis deprecated. GEP-1619 moves session persistence configuration to theBackendresource, and the route-level field will be removed onceBackendsession persistence reaches the Standard channel. Expect to migrate your routes to the new API in a future release.
Install NGINX Gateway Fabric with experimental features enabled.
To use Gateway API experimental resources, the Gateway API resources from the experimental channel must be installed before deploying NGINX Gateway Fabric. Additionally, NGINX Gateway Fabric must have experimental features enabled.
CautionAs noted in the Gateway API documentation, future releases of the Gateway API can include breaking changes to experimental resources and fields.
To install the Gateway API resources from the experimental channel, run the following:
kubectl kustomize "https://github.com/nginx/nginx-gateway-fabric/config/crd/gateway-api/experimental?ref=v2.8.0" | kubectl apply -f -If you plan to use theedgeversion of NGINX Gateway Fabric, you can replace the version inrefwithmain, for exampleref=main.
To enable experimental features on NGINX Gateway Fabric:
Using Helm: Set nginxGateway.gwAPIExperimentalFeatures.enable to true. An example can be found
in the Installation with Helm guide.
Using Kubernetes manifests: Add the --gateway-api-experimental-features command-line flag to the deployment manifest args.
An example can be found in the Installation with Kubernetes manifests guide.
Create the tea and latte applications:
kubectl apply -f - <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: tea
spec:
replicas: 2
selector:
matchLabels:
app: tea
template:
metadata:
labels:
app: tea
spec:
containers:
- name: tea
image: nginxdemos/nginx-hello:plain-text
ports:
- containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: tea
spec:
ports:
- port: 80
targetPort: 8080
protocol: TCP
name: http
selector:
app: tea
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: latte
spec:
replicas: 2
selector:
matchLabels:
app: latte
template:
metadata:
labels:
app: latte
spec:
containers:
- name: latte
image: nginxdemos/nginx-hello:plain-text
ports:
- containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: latte
spec:
ports:
- port: 80
targetPort: 8080
protocol: TCP
name: http
selector:
app: latte
EOFThis creates two Service resources and multiple Pods in the default namespace. The multiple replicas are needed to demonstrate stickiness to backend Pods.
kubectl get all -o wideNAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
pod/latte-d5f64f67f-9t2j5 1/1 Running 0 3h19m 10.244.0.96 kind-control-plane <none> <none>
pod/latte-d5f64f67f-drwc6 1/1 Running 0 3h19m 10.244.0.98 kind-control-plane <none> <none>
pod/tea-859766c68c-cnb8n 1/1 Running 0 3h19m 10.244.0.93 kind-control-plane <none> <none>
pod/tea-859766c68c-kttkb 1/1 Running 0 3h19m 10.244.0.97 kind-control-plane <none> <none>
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
service/latte ClusterIP 10.96.42.39 <none> 80/TCP 3h19m app=latte
service/tea ClusterIP 10.96.81.103 <none> 80/TCP 3h19m app=teaCreate a Gateway:
kubectl apply -f - <<EOF
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: gateway
spec:
gatewayClassName: nginx
listeners:
- name: http
port: 80
protocol: HTTP
hostname: "*.example.com"
EOFAfter creating the Gateway resource, NGINX Gateway Fabric will provision an NGINX Pod and Service fronting it to route traffic. Verify the gateway is created:
kubectl describe gateways.gateway.networking.k8s.io gatewayVerify the status is Accepted:
Status:
Addresses:
Type: IPAddress
Value: 10.96.36.219
Conditions:
Last Transition Time: 2026-01-09T05:40:37Z
Message: The Gateway is accepted
Observed Generation: 1
Reason: Accepted
Status: True
Type: Accepted
Last Transition Time: 2026-01-09T05:40:37Z
Message: The Gateway is programmed
Observed Generation: 1
Reason: Programmed
Status: True
Type: ProgrammedSave the public IP address and port(s) of the Gateway into shell variables:
GW_IP=XXX.YYY.ZZZ.III
GW_PORT=<PORT_NUMBER>Replace <PORT_NUMBER> with the port number you use to access your Gateway.
In a production environment, you should have a DNS record for the external IP address that is exposed, and it should refer to the hostname that the gateway will forward for.
You can configure session persistence by specifying the sessionPersistence field on an HTTPRouteRule or GRPCRouteRule. This configuration is translated to the sticky cookie directive on the NGINX data plane. In this guide, you’ll create a tea HTTPRoute with sessionPersistence configured at the rule level and then verify how traffic behaves when the route has multiple backend Pods.
kubectl apply -f - <<EOF
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: tea
spec:
parentRefs:
- name: gateway
sectionName: http
hostnames:
- "cafe.example.com"
rules:
- matches:
- path:
type: Exact
value: /tea
backendRefs:
- name: tea
port: 80
sessionPersistence:
sessionName: "cookie-tea"
type: Cookie
absoluteTimeout: 24h
cookieConfig:
lifetimeType: Permanent
EOFVerify the tea HTTPRoute is Accepted:
Status:
Parents:
Conditions:
Last Transition Time: 2025-12-10T00:15:12Z
Message: The Route is accepted
Observed Generation: 1
Reason: Accepted
Status: True
Type: Accepted
Last Transition Time: 2025-12-10T00:15:12Z
Message: All references are resolved
Observed Generation: 1
Reason: ResolvedRefs
Status: True
Type: ResolvedRefs
Controller Name: gateway.nginx.org/nginx-gateway-controllerNext, verify that the tea upstream has a sticky cookie directive configured, which is responsible for issuing the session cookie and its attributes. The sticky cookie directive’s attributes are derived from the sessionPersistence configuration, such as the expiry (24h) and the route path (/tea). Inspect the NGINX configuration with:
kubectl exec -it deployments/gateway-nginx -- nginx -Tupstream default_tea_80_tea_default_0 {
random two least_conn;
zone default_tea_80_tea_default_0 1m;
sticky cookie cookie-tea expires=24h path=/tea;
state /var/lib/nginx/state/default_tea_80.conf;
}In this example, the tea Service has two backend Pods with IPs 10.244.0.93 and 10.244.0.97. We’ll send five requests to the /tea endpoint and observe that all responses are served by the same backend Pod, demonstrating cookie-based session persistence.
First, send a request to /tea and store the session cookie:
curl -v -c /tmp/tea-cookies.txt \
-H "Host: cafe.example.com" \
http://localhost:8080/teaYou’ll see a cookie being set, for example:
* Added cookie cookie-tea="2878e97a4c7a8406b791aa0bd0b2f145" for domain cafe.example.com, path /tea, expire 1765417195
< Set-Cookie: cookie-tea=2878e97a4c7a8406b791aa0bd0b2f145; expires=Thu, 11-Dec-25 01:39:55 GMT; max-age=86400; path=/teaNext, send five requests using the stored cookie:
for i in $(seq 5); do
echo "Request #$i"
curl -s -b /tmp/tea-cookies.txt \
-H "Host: cafe.example.com" \
http://localhost:8080/tea \
| grep -E 'Server (address|name)'
echo
doneAll responses are served by the same backend Pod, tea-859766c68c-cnb8n with IP 10.244.0.93:8080, confirming session persistence:
Request #1
Server address: 10.244.0.93:8080
Server name: tea-859766c68c-cnb8n
Request #2
Server address: 10.244.0.93:8080
Server name: tea-859766c68c-cnb8n
Request #3
Server address: 10.244.0.93:8080
Server name: tea-859766c68c-cnb8n
Request #4
Server address: 10.244.0.93:8080
Server name: tea-859766c68c-cnb8n
Request #5
Server address: 10.244.0.93:8080
Server name: tea-859766c68c-cnb8nWe’ll create routing rules for the latte application without any session affinity or persistence settings and then verify how the traffic behaves.
Let’s create the latte HTTPRoute:
kubectl apply -f - <<EOF
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: latte
spec:
parentRefs:
- name: gateway
sectionName: http
hostnames:
- "cafe.example.com"
rules:
- matches:
- path:
type: Exact
value: /latte
backendRefs:
- name: latte
port: 80
EOFVerify the NGINX configuration:
kubectl exec -it deployments/gateway -- nginx -Tupstream default_latte_80 {
random two least_conn;
zone default_latte_80 1m;
state /var/lib/nginx/state/default_latte_80.conf;
}In this example, the latte Service currently has two backend Pods with IPs 10.244.0.96 and 10.244.0.98. We’ll send five requests to the /latte endpoint and observe which backend Pod serves each response to understand how a regular backend behaves without any session affinity or persistence configured.
for i in $(seq 5); do
echo "Request #$i"
curl -s -H "Host: cafe.example.com" \
http://localhost:8080/latte \
| grep -E 'Server (address|name)'
echo
doneYou will see responses coming from both backend Pods, for example:
Request #1
Server address: 10.244.0.98:8080
Server name: latte-d5f64f67f-drwc6
Request #2
Server address: 10.244.0.96:8080
Server name: latte-d5f64f67f-9t2j5
Request #3
Server address: 10.244.0.98:8080
Server name: latte-d5f64f67f-drwc6
Request #4
Server address: 10.244.0.98:8080
Server name: latte-d5f64f67f-drwc6
Request #5
Server address: 10.244.0.96:8080
Server name: latte-d5f64f67f-9t2j5Because there is no session persistence configured for latte, traffic is distributed across both backend Pods according to the default load-balancing method, and requests from the same client are not guaranteed to hit the same Pod.
- Session Persistence.
- API reference: all configuration fields for the
UpstreamSettingsPolicyAPI.