The NGINX push stream module turns your NGINX server into a real-time messaging hub. It implements HTTP push technology using the publish/subscribe (pub/sub) pattern — entirely within NGINX. You do not need an external message broker like Redis, RabbitMQ, or Kafka.
This module supports multiple transport protocols out of the box: WebSocket, Server-Sent Events (EventSource), long polling, HTTP streaming, and Forever iFrame. Publishers send messages to named channels. All connected subscribers receive those messages instantly.
If your application needs real-time notifications, live dashboards, chat, or server-push updates, the NGINX push stream module delivers this at the web server layer. For a comparison with another NGINX-native approach, see our article on NGINX Nchan.
Why Use the NGINX Push Stream Module?
Traditional real-time messaging requires a separate service alongside NGINX. Examples include a Node.js WebSocket server or a Redis pub/sub broker. The push stream module eliminates that extra layer:
- No external dependencies — everything runs inside NGINX’s event loop
- Shared memory architecture — channels and messages live in shared memory across all workers
- Native protocol support — WebSocket, EventSource, long polling, and streaming work without extra code
- Channel management — built-in statistics, automatic cleanup, and message TTL
- Scalable — NGINX’s asynchronous architecture handles thousands of concurrent subscribers
When to Use This Module vs. External Services
Choose the push stream module for lightweight pub/sub on a single server or cluster. For distributed systems needing message persistence or cross-datacenter replication, use a dedicated broker (Redis Streams, Kafka, RabbitMQ).
Installation
RHEL, CentOS, AlmaLinux, Rocky Linux
Install the module from the GetPageSpeed RPM repository:
sudo dnf install https://extras.getpagespeed.com/release-latest.rpm
sudo dnf install nginx-module-push-stream
Then load the module. Add this line at the top of /etc/nginx/nginx.conf:
load_module modules/ngx_http_push_stream_module.so;
Debian and Ubuntu
First, set up the GetPageSpeed APT repository, then install:
sudo apt-get update
sudo apt-get install nginx-module-push-stream
On Debian/Ubuntu, the package handles module loading automatically. No
load_moduledirective is needed.
Find additional details on the RPM module page or the APT module page.
Core Concepts
Before diving into configuration, understand the three endpoint types:
- Publisher — accepts POST requests to push messages and DELETE requests to remove channels
- Subscriber — holds open connections and delivers messages in real time
- Statistics — returns JSON metadata about channels, subscribers, and message counts
Each endpoint is activated by its directive inside an NGINX location block. Channels are identified by a path or query parameter.
Basic Configuration
Here is a minimal working configuration with all three endpoint types:
http {
push_stream_shared_memory_size 32M;
server {
listen 80;
# Publisher (admin mode — allows GET, POST, DELETE)
location /pub {
push_stream_publisher admin;
push_stream_channels_path $arg_id;
push_stream_store_messages on;
}
# Subscriber (streaming mode)
location ~ /sub/(.*) {
push_stream_subscriber;
push_stream_channels_path $1;
push_stream_message_template "~text~\n";
}
# Channel statistics
location /channels-stats {
push_stream_channels_statistics;
push_stream_channels_path $arg_id;
}
}
}
Test the configuration and reload NGINX:
nginx -t && systemctl reload nginx
Quick Pub/Sub Test
Open two terminal windows. In the first, start a streaming subscriber:
curl -s --no-buffer 'http://localhost/sub/my_channel'
In the second terminal, publish a message:
curl -s -X POST 'http://localhost/pub?id=my_channel' -d 'Hello, real-time world!'
The subscriber terminal immediately displays the message. The publisher receives JSON:
{"channel": "my_channel", "published_messages": 1, "stored_messages": 1, "subscribers": 1}
Subscriber Transport Modes
The module supports five subscriber modes. Each is activated by passing a mode argument to push_stream_subscriber.
Streaming (Default)
The default mode holds the HTTP connection open. It pushes each new message as it arrives:
location ~ /stream/(.*) {
push_stream_subscriber streaming;
push_stream_channels_path $1;
push_stream_subscriber_connection_ttl 30s;
push_stream_ping_message_interval 10s;
}
Use streaming for dashboards and monitoring UIs.
EventSource (Server-Sent Events)
The eventsource mode formats messages per the W3C SSE specification. It works with the browser EventSource API:
location ~ /events/(.*) {
push_stream_subscriber eventsource;
push_stream_channels_path $1;
push_stream_ping_message_interval 10s;
push_stream_subscriber_connection_ttl 60s;
}
On the client side, integration is straightforward:
const source = new EventSource('/events/notifications');
source.onmessage = function(event) {
console.log('Received:', event.data);
};
EventSource includes automatic reconnection. The browser reconnects and resumes from the last event ID if the connection drops.
WebSocket
The websocket mode performs the HTTP Upgrade handshake. It communicates over a persistent connection. For more about WebSocket proxying in NGINX, see our guide.
location ~ /ws/(.*) {
push_stream_subscriber websocket;
push_stream_channels_path $1;
push_stream_websocket_allow_publish on;
push_stream_ping_message_interval 10s;
push_stream_subscriber_connection_ttl 60s;
push_stream_message_template "~text~";
}
With push_stream_websocket_allow_publish enabled, clients send and receive messages through one connection. This enables true bidirectional communication.
Long Polling
The long-polling mode holds the connection until a message arrives, then closes it. The client must reconnect for the next message:
location ~ /poll/(.*) {
push_stream_subscriber long-polling;
push_stream_channels_path $1;
push_stream_longpolling_connection_ttl 30s;
push_stream_message_template "{\"id\":~id~,\"text\":\"~text~\"}";
}
Long polling works behind restrictive proxies or firewalls. Use it when WebSocket and EventSource are unavailable.
Polling
The polling mode returns immediately with any available message. It does not hold the connection open:
location ~ /check/(.*) {
push_stream_subscriber polling;
push_stream_channels_path $1;
}
Only use polling when no other mode is feasible. It generates the most HTTP overhead. The response returns HTTP 304 when no new messages are available.
Publisher Modes
The push_stream_publisher directive accepts two modes:
normal(default) — allows POST (publish) onlyadmin— allows POST, GET (channel info), and DELETE (channel removal)
# Normal publisher — POST only
location /publish {
push_stream_publisher normal;
push_stream_channels_path $arg_id;
push_stream_store_messages on;
}
# Admin publisher — POST, GET, DELETE
location /admin/pub {
push_stream_publisher admin;
push_stream_channels_path $arg_id;
push_stream_store_messages on;
}
With the admin publisher, you manage channels via HTTP:
# Publish a message
curl -X POST 'http://localhost/admin/pub?id=alerts' -d 'Server rebooting in 5 minutes'
# Get channel info
curl 'http://localhost/admin/pub?id=alerts'
# Delete a channel (disconnects all subscribers)
curl -X DELETE 'http://localhost/admin/pub?id=alerts'
Channel Statistics
The push_stream_channels_statistics directive provides monitoring data in JSON:
location /channels-stats {
push_stream_channels_statistics;
push_stream_channels_path $arg_id;
}
Query global or per-channel statistics:
# All channels summary
curl -s 'http://localhost/channels-stats'
# Specific channel
curl -s 'http://localhost/channels-stats?id=alerts'
# All channels detailed
curl -s 'http://localhost/channels-stats?id=ALL'
# Channels matching a prefix
curl -s 'http://localhost/channels-stats?id=user_*'
Example global statistics response:
{
"hostname": "web01",
"time": "2026-03-18T10:30:00",
"channels": 5,
"wildcard_channels": 0,
"published_messages": 142,
"stored_messages": 42,
"messages_in_trash": 0,
"channels_in_delete": 0,
"channels_in_trash": 0,
"subscribers": 23,
"uptime": 3600
}
Message Templates
Control message formatting with the push_stream_message_template directive. The default is ~text~, outputting the raw message body.
Available template variables:
| Variable | Description |
|---|---|
~id~ |
Message sequence ID |
~text~ |
Message body |
~channel~ |
Channel name |
~tag~ |
Message tag (for deduplication) |
~time~ |
Message timestamp |
~event_id~ |
Event ID (for EventSource) |
~event_type~ |
Event type (for EventSource) |
~size~ |
Message size in bytes |
JSON Output Template
For API-style JSON output, combine multiple variables:
push_stream_message_template "{\"id\":~id~,\"channel\":\"~channel~\",\"text\":\"~text~\",\"tag\":~tag~}";
Header and Footer Templates
Define templates sent on subscriber connect and disconnect:
location ~ /sub/(.*) {
push_stream_subscriber;
push_stream_channels_path $1;
push_stream_header_template "{\"status\":\"connected\"}\n";
push_stream_footer_template "{\"status\":\"disconnected\"}\n";
push_stream_message_template "{\"data\":\"~text~\"}\n";
push_stream_subscriber_connection_ttl 30s;
}
When a subscriber connects, it first receives {"status":"connected"}. Each message arrives as {"data":"..."}. When the connection ends, the footer {"status":"disconnected"} is sent.
For file-based headers, use push_stream_header_template_file:
push_stream_header_template_file /etc/nginx/push-stream-header.html;
Multi-Channel Subscriptions
Subscribers can listen to multiple channels at once. Include multiple names in the URL path, separated by slashes:
location ~ /sub/(.*) {
push_stream_subscriber;
push_stream_channels_path $1;
}
# Subscribe to three channels at once
curl -s --no-buffer 'http://localhost/sub/alerts/notifications/system'
Messages from any of the three channels arrive through one connection. Use the ~channel~ template variable to identify the source.
Message History and Resumption
When push_stream_store_messages is enabled on the publisher, messages are stored in shared memory up to the push_stream_message_ttl limit. Stored messages are not automatically sent to new subscribers. Instead, subscribers retrieve history by sending tracking headers.
Use If-Modified-Since and If-None-Match to resume from a specific point:
# Get all messages since epoch (full backlog)
curl -s -H "If-Modified-Since: Thu, 01 Jan 1970 00:00:00 GMT" \
-H "If-None-Match: 0" \
'http://localhost/sub/my_channel'
For EventSource subscribers, configure push_stream_last_event_id. The browser EventSource API handles reconnection and history resumption automatically via the Last-Event-ID header.
location ~ /events/(.*) {
push_stream_subscriber eventsource;
push_stream_channels_path $1;
push_stream_last_event_id $arg_lastEventId;
}
Wildcard Channels
Channels whose names begin with push_stream_wildcard_channel_prefix are classified as wildcard channels. They behave identically to normal channels for messaging. The distinction matters only with push_stream_authorized_channels_only enabled.
With authorized_channels_only on, normal channels must exist before a subscriber can connect. Wildcard channels bypass this restriction — subscribers auto-create them. This is useful for restricting access to known channels while allowing dynamic prefixed ones.
Important: To subscribe to a wildcard channel, the request must also include at least one normal channel. A subscriber cannot connect to only wildcard channels.
http {
push_stream_shared_memory_size 32M;
push_stream_wildcard_channel_prefix "user_";
push_stream_max_number_of_wildcard_channels 100;
server {
listen 80;
location /pub {
push_stream_publisher admin;
push_stream_channels_path $arg_id;
push_stream_store_messages on;
}
location ~ /sub/(.*) {
push_stream_subscriber;
push_stream_channels_path $1;
push_stream_authorized_channels_only on;
push_stream_wildcard_channel_max_qtd 3;
}
}
}
In this setup, subscribing to /sub/system_alerts requires that channel to exist. But a request like /sub/main_feed/user_123 works even if user_123 does not exist yet — the normal channel main_feed satisfies the requirement while user_123 is auto-created as a wildcard channel.
Use push_stream_wildcard_channel_max_qtd to limit how many wildcard channels one subscriber can join per request.
Directive Reference
Global Directives (http block)
These directives must appear in the http block:
| Directive | Default | Description |
|---|---|---|
push_stream_shared_memory_size |
required | Shared memory zone size |
push_stream_message_ttl |
30m |
How long messages are kept |
push_stream_channel_inactivity_time |
30s |
Idle channel removal time |
push_stream_max_subscribers_per_channel |
unlimited | Max subscribers per channel |
push_stream_max_messages_stored_per_channel |
unlimited | Max stored messages per channel |
push_stream_max_channel_id_length |
unlimited | Max channel name length |
push_stream_max_number_of_channels |
unlimited | Max total channels |
push_stream_max_number_of_wildcard_channels |
unlimited | Max wildcard channels |
push_stream_wildcard_channel_prefix |
empty | Wildcard channel prefix string |
push_stream_channel_deleted_message_text |
"Channel deleted" |
Message on channel deletion |
push_stream_ping_message_text |
empty | Keep-alive ping content |
push_stream_timeout_with_body |
off |
Send body on timeout |
push_stream_events_channel_id |
empty | Internal events channel name |
Location Directives
These directives work in http, server, or location blocks:
| Directive | Default | Description |
|---|---|---|
push_stream_channels_path |
required | Channel ID from a variable |
push_stream_store_messages |
off |
Store messages for history retrieval |
push_stream_channel_info_on_publish |
on |
Return metadata after publish |
push_stream_authorized_channels_only |
off |
Reject nonexistent channels (403) |
push_stream_message_template |
~text~ |
Message format template |
push_stream_header_template |
empty | Template on connect |
push_stream_header_template_file |
empty | File-based header template |
push_stream_footer_template |
empty | Template on disconnect |
push_stream_ping_message_interval |
disabled | Keep-alive ping interval |
push_stream_subscriber_connection_ttl |
unlimited | Max connection lifetime |
push_stream_longpolling_connection_ttl |
unlimited | Max long-polling lifetime |
push_stream_websocket_allow_publish |
off |
Allow WebSocket publishing |
push_stream_wildcard_channel_max_qtd |
unlimited | Max wildcards per subscriber |
push_stream_allowed_origins |
empty | CORS allowed origins |
push_stream_allow_connections_to_events_channel |
off |
Allow events channel access |
Tracking Directives
These accept NGINX variables for message history resumption:
| Directive | Description |
|---|---|
push_stream_last_received_message_time |
Last received message timestamp |
push_stream_last_received_message_tag |
Last received message tag |
push_stream_last_event_id |
Last EventSource event ID |
push_stream_user_agent |
Subscriber’s User-Agent |
push_stream_padding_by_user_agent |
Response padding rules by User-Agent |
Production Configuration Example
Here is a production-ready setup with security restrictions. For custom authentication logic, see our Lua module guide.
http {
push_stream_shared_memory_size 128M;
push_stream_message_ttl 10m;
push_stream_channel_inactivity_time 60s;
push_stream_max_channel_id_length 200;
push_stream_max_messages_stored_per_channel 50;
push_stream_max_subscribers_per_channel 500;
push_stream_ping_message_text "";
server {
listen 443 ssl;
server_name push.example.com;
ssl_certificate /etc/pki/tls/certs/push.example.com.crt;
ssl_certificate_key /etc/pki/tls/private/push.example.com.key;
# Internal publisher — backend servers only
location /internal/pub {
allow 10.0.0.0/8;
deny all;
push_stream_publisher admin;
push_stream_channels_path $arg_id;
push_stream_store_messages on;
push_stream_channel_info_on_publish on;
}
# Public EventSource subscriber
location ~ /events/(.*) {
push_stream_subscriber eventsource;
push_stream_channels_path $1;
push_stream_ping_message_interval 15s;
push_stream_subscriber_connection_ttl 300s;
push_stream_authorized_channels_only on;
push_stream_allowed_origins "https://example.com";
push_stream_last_event_id $arg_lastEventId;
}
# Public WebSocket subscriber
location ~ /ws/(.*) {
push_stream_subscriber websocket;
push_stream_channels_path $1;
push_stream_ping_message_interval 15s;
push_stream_subscriber_connection_ttl 300s;
push_stream_authorized_channels_only on;
push_stream_message_template "{\"id\":~id~,\"channel\":\"~channel~\",\"text\":\"~text~\"}";
}
# Internal statistics
location /internal/stats {
allow 10.0.0.0/8;
deny all;
push_stream_channels_statistics;
push_stream_channels_path $arg_id;
}
}
}
Key points in this configuration:
- Publisher is internal only — restricted via
allow/deny - Statistics are internal only — prevents information leakage
authorized_channels_onlyis on — returns 403 for nonexistent channels- CORS configured — restricts connecting domains
- Connection TTL set — prevents zombie connections
- Keep-alive pings — prevent proxies from closing idle connections
Security Best Practices
Restrict Publisher Access
Never expose the publisher to the public internet. Use allow/deny or authentication:
location /pub {
auth_basic "Publisher Access";
auth_basic_user_file /etc/nginx/.htpasswd;
push_stream_publisher admin;
push_stream_channels_path $arg_id;
push_stream_store_messages on;
}
Use Authorized Channels Only
Enable push_stream_authorized_channels_only on subscriber locations. This prevents arbitrary channel creation:
location ~ /sub/(.*) {
push_stream_subscriber;
push_stream_channels_path $1;
push_stream_authorized_channels_only on;
}
Subscribing to a nonexistent channel returns 403 Forbidden. The response header reads: X-Nginx-PushStream-Explain: Subscriber could not create channels.
Limit Channel and Message Resources
Set resource limits to prevent abuse:
push_stream_max_number_of_channels 1000;
push_stream_max_subscribers_per_channel 200;
push_stream_max_messages_stored_per_channel 50;
push_stream_max_channel_id_length 100;
Disable WebSocket Publishing by Default
The push_stream_websocket_allow_publish directive defaults to off. Enable it only for bidirectional WebSocket use cases. Keep publishing restricted to your backend otherwise.
Performance Considerations
Memory Sizing
The push_stream_shared_memory_size directive controls memory allocation. Per-object memory usage:
- 200 bytes per message (shared memory)
- 270 bytes per channel (shared memory)
- 160 bytes per subscriber (shared memory) plus ~6.5 KB (system memory)
Example for 1,000 channels, 50 messages each, and 5,000 subscribers:
- Channel memory: 1,000 × 270 = 263 KB
- Message memory: 50,000 × 200 = 9.5 MB
- Subscriber shared memory: 5,000 × 160 = 781 KB
- Total shared memory: ~11 MB (use
push_stream_shared_memory_size 16M) - System memory: 5,000 × 6.5 KB = ~32 MB (outside shared memory)
Message TTL and Channel Cleanup
Keep push_stream_message_ttl as short as your application allows. Expired messages free shared memory automatically. The push_stream_channel_inactivity_time removes idle channels.
Connection TTL and Ping Intervals
Set push_stream_subscriber_connection_ttl to prevent zombie connections. Combine it with push_stream_ping_message_interval to keep connections alive through proxies.
Troubleshooting
“push_stream_shared_memory_size must be set”
Add the required directive in the http block:
http {
push_stream_shared_memory_size 32M;
# ...
}
“Subscriber could not create channels” (403)
This appears when push_stream_authorized_channels_only is on. The channel does not exist yet. Ensure the publisher creates the channel first, or use a wildcard channel prefix for auto-creation.
New Subscribers Not Receiving Past Messages
Stored messages require tracking headers for retrieval. Send If-Modified-Since and If-None-Match headers to receive history. Without these headers, subscribers only get messages published after they connect. See the “Message History and Resumption” section.
WebSocket Connections Close Immediately
Check that push_stream_subscriber_connection_ttl is not too low. Also verify that any reverse proxy passes upgrade headers:
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
Shared Memory Exhaustion
Monitor stored_messages and channels via the statistics endpoint. Increase push_stream_shared_memory_size or decrease push_stream_message_ttl if needed.
Conclusion
The NGINX push stream module provides a lightweight solution for real-time pub/sub messaging. It supports WebSocket, EventSource, long polling, and streaming — all without external dependencies. For media streaming, explore our guides on NGINX HTTP-FLV and Kaltura adaptive streaming.
Visit the module’s GitHub repository for details. Install the pre-built package from the GetPageSpeed RPM repository or the GetPageSpeed APT repository.

