proxy-buffering
The proxy-buffering plugin disables the NGINX proxy_buffering directive for selected routes. This allows incremental responses to reach clients as the upstream sends them instead of waiting in the proxy buffer.
This behavior is useful for server-sent events (SSE), watch APIs, and other upstream services that send incremental or chunked responses.
Examples
Compare Buffered and Incremental Delivery
The following example compares NGINX's default response buffering with a route that disables buffering through proxy-buffering. A test upstream declares a fixed response length and sends three lines at two-second intervals. The buffered route delivers all three lines together, while the route with buffering disabled delivers each line as it arrives.
Create the following Python script for the streaming upstream:
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import time
class Handler(BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1"
def do_GET(self):
chunks = [b"one\n", b"two\n", b"three\n"]
self.send_response(200)
self.send_header("Content-Type", "text/plain")
self.send_header("Content-Length", str(sum(map(len, chunks))))
self.send_header("Connection", "close")
self.end_headers()
for chunk in chunks:
self.wfile.write(chunk)
self.wfile.flush()
time.sleep(2)
self.close_connection = True
def log_message(self, *_):
pass
ThreadingHTTPServer(("0.0.0.0", 8000), Handler).serve_forever()
Start the upstream in the environment used by the gateway:
- Docker
- Kubernetes
Set GATEWAY_CONTAINER to the running APISIX or API7 Gateway container. Create a dedicated network and connect the gateway to it:
export GATEWAY_CONTAINER=replace-with-gateway-container-name
docker network create gateway-stream-net
docker network connect gateway-stream-net "$GATEWAY_CONTAINER"
Start the streaming server from the directory containing stream-server.py:
docker run -d \
--name stream-server \
--network gateway-stream-net \
-v "$PWD/stream-server.py:/stream-server.py:ro" \
python:3.13.15-alpine3.24 \
python -u /stream-server.py
Create the streaming server configuration, Deployment, and Service:
apiVersion: v1
kind: ConfigMap
metadata:
namespace: aic
name: stream-server
data:
stream-server.py: |
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import time
class Handler(BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1"
def do_GET(self):
chunks = [b"one\n", b"two\n", b"three\n"]
self.send_response(200)
self.send_header("Content-Type", "text/plain")
self.send_header("Content-Length", str(sum(map(len, chunks))))
self.send_header("Connection", "close")
self.end_headers()
for chunk in chunks:
self.wfile.write(chunk)
self.wfile.flush()
time.sleep(2)
self.close_connection = True
def log_message(self, *_):
pass
ThreadingHTTPServer(("0.0.0.0", 8000), Handler).serve_forever()
---
apiVersion: apps/v1
kind: Deployment
metadata:
namespace: aic
name: stream-server
spec:
replicas: 1
selector:
matchLabels:
app: stream-server
template:
metadata:
labels:
app: stream-server
spec:
containers:
- name: stream-server
image: python:3.13.15-alpine3.24
command:
- python
- -u
- /stream-server.py
ports:
- name: http
containerPort: 8000
readinessProbe:
tcpSocket:
port: http
volumeMounts:
- name: script
mountPath: /stream-server.py
subPath: stream-server.py
volumes:
- name: script
configMap:
name: stream-server
---
apiVersion: v1
kind: Service
metadata:
namespace: aic
name: stream-server
spec:
selector:
app: stream-server
ports:
- name: http
port: 8000
targetPort: http
Apply the manifest and wait for the Deployment to become available:
kubectl apply -f stream-server.yaml
kubectl rollout status deployment/stream-server -n aic
Create two routes to the same upstream. The first route uses the default NGINX buffering behavior, and the second disables buffering with proxy-buffering:
- APISIX Admin API
- ADC
- Ingress Controller (Kubernetes)
Create the baseline route without proxy-buffering:
curl "http://127.0.0.1:9180/apisix/admin/routes/proxy-buffering-on" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"uri": "/buffered",
"upstream": {
"type": "roundrobin",
"nodes": {
"stream-server:8000": 1
}
}
}'
Create the comparison route with buffering disabled:
curl "http://127.0.0.1:9180/apisix/admin/routes/proxy-buffering-off" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"uri": "/unbuffered",
"plugins": {
"proxy-buffering": {
"disable_proxy_buffering": true
}
},
"upstream": {
"type": "roundrobin",
"nodes": {
"stream-server:8000": 1
}
}
}'
Create one service with a baseline route and a route that disables buffering:
services:
- name: proxy-buffering-service
labels:
docs-example: proxy-buffering
routes:
- name: proxy-buffering-on
uris:
- /buffered
- name: proxy-buffering-off
uris:
- /unbuffered
plugins:
proxy-buffering:
disable_proxy_buffering: true
upstream:
type: roundrobin
nodes:
- host: stream-server
port: 8000
weight: 1
Preview the changes for this example:
adc diff -f adc.yaml \
--include-resource-type service \
--label-selector docs-example=proxy-buffering
Review the diff, then synchronize only the labeled service:
adc sync -f adc.yaml \
--include-resource-type service \
--label-selector docs-example=proxy-buffering
- Gateway API
- APISIX CRD
Create a plugin configuration and a route with one rule for each path:
apiVersion: apisix.apache.org/v1alpha1
kind: PluginConfig
metadata:
namespace: aic
name: proxy-buffering-off
spec:
plugins:
- name: proxy-buffering
config:
disable_proxy_buffering: true
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
namespace: aic
name: proxy-buffering
spec:
parentRefs:
- name: apisix
rules:
- matches:
- path:
type: Exact
value: /buffered
backendRefs:
- name: stream-server
port: 8000
- matches:
- path:
type: Exact
value: /unbuffered
filters:
- type: ExtensionRef
extensionRef:
group: apisix.apache.org
kind: PluginConfig
name: proxy-buffering-off
backendRefs:
- name: stream-server
port: 8000
Create an ApisixRoute with a baseline route and a route that disables buffering:
apiVersion: apisix.apache.org/v2
kind: ApisixRoute
metadata:
namespace: aic
name: proxy-buffering
spec:
ingressClassName: apisix
http:
- name: proxy-buffering-on
match:
paths:
- /buffered
upstreams:
- serviceName: stream-server
servicePort: 8000
- name: proxy-buffering-off
match:
paths:
- /unbuffered
upstreams:
- serviceName: stream-server
servicePort: 8000
plugins:
- name: proxy-buffering
config:
disable_proxy_buffering: true
Apply the Ingress Controller configuration:
kubectl apply -f proxy-buffering-ic.yaml
Create a Python client that records when each response line becomes available:
import time
import urllib.request
for path in ("buffered", "unbuffered"):
start = time.monotonic()
print(path)
with urllib.request.urlopen(f"http://127.0.0.1:9080/{path}") as response:
for line in response:
elapsed = time.monotonic() - start
print(f"{elapsed:.1f}s {line.decode().strip()}")
Run the client:
python stream-client.py
The baseline route should release all three lines together after about four seconds. The route with proxy-buffering should release each line as the upstream writes it:
buffered
4.0s one
4.0s two
4.0s three
unbuffered
0.0s one
2.0s two
4.0s three
This difference confirms that proxy-buffering disables NGINX response buffering for the configured route.