Skip to main content

NGINX / Server Setup

NGINX Push Stream Module: Real-Time Pub/Sub Guide

by ,


We have by far the largest RPM repository with NGINX module packages and VMODs for Varnish. If you want to install NGINX, Varnish, and lots of useful performance/security software with smooth yum upgrades for production use, this is the repository for you.
Active subscription is required.

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_module directive 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:

  1. Publisher — accepts POST requests to push messages and DELETE requests to remove channels
  2. Subscriber — holds open connections and delivers messages in real time
  3. 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) only
  • admin — 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~}";

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_only is 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.

D

Danila Vershinin

Founder & Lead Engineer

NGINX configuration and optimizationLinux system administrationWeb performance engineering

10+ years NGINX experience • Maintainer of GetPageSpeed RPM repository • Contributor to open-source NGINX modules

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes:

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

This site uses Akismet to reduce spam. Learn how your comment data is processed.