If you're running a self-hosted Supabase instance, you've probably wondered about Realtime—that magical service that powers live updates, presence indicators, and real-time collaboration features. While the hosted Supabase platform handles Realtime configuration automatically, self-hosters need to understand the moving parts to get it working reliably in production.
This guide covers everything you need to know about configuring Supabase Realtime on your own infrastructure, from basic setup to production optimization.
What Is Supabase Realtime?
Supabase Realtime is a globally distributed Elixir cluster built with the Phoenix Framework. It provides three core features:
- Broadcast: Send ephemeral messages between clients with low latency
- Presence: Track and synchronize shared state between connected clients
- Postgres Changes: Listen to database changes and push them to authorized clients via WebSockets
The Elixir/Phoenix stack is a deliberate choice—Phoenix can handle millions of concurrent connections because Elixir provides lightweight processes (not OS processes). This makes Realtime remarkably efficient for its job.
For self-hosters, Realtime comes bundled in the official Docker Compose stack. But "it's included" doesn't mean "it's production-ready." Let's dig into the configuration details.
Essential Environment Variables
The Realtime service accepts configuration through environment variables. Here are the ones that matter most for self-hosted deployments.
Database Connection
# Connect to your Postgres database DB_HOST=db DB_PORT=5432 DB_USER=supabase_admin DB_PASSWORD=your-database-password DB_NAME=postgres # Prevent NXDOMAIN errors when using hostnames DB_IP_VERSION=ipv4
The DB_IP_VERSION setting is often overlooked. If your database host is a domain name rather than an IP address, set this explicitly to prevent intermittent DNS resolution failures.
Security Configuration
# JWT validation (must match your Supabase JWT secret) API_JWT_SECRET=your-jwt-secret # Encryption key for Realtime/Supavisor (minimum 64 characters) SECRET_KEY_BASE=$(openssl rand -base64 48)
The SECRET_KEY_BASE secures communications between Realtime and Supavisor. Generate it properly—don't copy a value from example configs or tutorials.
Replication Settings
# WAL tracking slot name (must be unique per Realtime instance) SLOT_NAME=supabase_realtime_replication_slot # Optional: custom suffix for multi-instance setups SLOT_NAME_SUFFIX=_production
The replication slot is how Realtime tracks database changes. If your Realtime server crashes, this slot preserves changes since the last committed position. However, this can also cause problems—more on that in the troubleshooting section.
Understanding Realtime Limits
Before deploying to production, understand the default limits baked into Realtime:
| Limit | Default Value |
|---|---|
| Maximum channels per tenant | 100 |
| Maximum concurrent users per channel | 200 |
| Maximum events per second per tenant | 100 |
| Maximum bytes per second per tenant | 100,000 |
These defaults work for many applications but will need adjustment for high-traffic use cases. You can modify them via environment variables:
MAX_CHANNELS_PER_TENANT=500 MAX_CONCURRENT_USERS_PER_CHANNEL=1000 MAX_EVENTS_PER_SECOND=500
Be cautious when raising limits—higher values mean more resource consumption. Test thoroughly before deploying changes.
The Postgres Changes Bottleneck
Here's something the documentation mentions but doesn't emphasize enough: Postgres Changes has a built-in scaling limitation.
When a subscribed user receives a change event, Realtime must verify that user has permission to see that row. If you have 100 users subscribed to a table and make one insert, Realtime triggers 100 read operations—one per user for authorization checks.
This isn't a bug; it's how Row Level Security enforcement works in Realtime. But it means:
- High-frequency writes to tables with many subscribers can overload your database
- Complex RLS policies compound the performance impact
- You need to design your subscription patterns carefully
Practical recommendations:
- Use Broadcast for ephemeral data that doesn't need persistence
- Limit Postgres Changes subscriptions to essential tables
- Consider using Presence for user status rather than database triggers
- For chat applications, batch messages rather than sending each keystroke
Common Self-Hosting Issues and Fixes
The RLIMIT_NOFILE Restart Loop
One frustrating issue causes Realtime to restart continuously with this error:
/app/run.sh: line 6: RLIMIT_NOFILE: unbound variable
This typically happens when Docker can't set file descriptor limits properly. The fix involves updating your docker-compose.yml:
realtime:
image: supabase/realtime:latest
environment:
- RLIMIT_NOFILE=10000
ulimits:
nofile:
soft: 10000
hard: 20000
If you're using Supascale to manage your deployment, this configuration is handled automatically—one less thing to debug at 2 AM.
WAL Buildup and Disk Space
The replication slot that tracks changes can cause Write-Ahead Log buildup if Realtime falls behind or disconnects. This can fill your disk and crash Postgres entirely.
Prevent this by setting a maximum WAL size in your Postgres configuration:
ALTER SYSTEM SET max_slot_wal_keep_size = '10GB'; SELECT pg_reload_conf();
This limits how much WAL data a slot can hold. If Realtime falls too far behind, it will need to recreate its slot, but your database won't crash.
Memory Issues from Replication Lag
If your Realtime server has less memory than the accumulated replication lag data, it will crash. This creates a cycle: crash, restart, try to catch up, run out of memory, crash again.
The solution is either:
- Add more memory to your Realtime container
- Set
max_slot_wal_keep_sizeto limit lag accumulation - Accept that a new replication slot will be created (missing some events)
For production deployments, monitor replication lag as a key metric.
Production Optimization Checklist
Before going live, verify these configurations:
Database settings:
-- Check replication slot exists SELECT * FROM pg_replication_slots WHERE slot_name LIKE 'supabase_realtime%'; -- Set WAL limits ALTER SYSTEM SET max_slot_wal_keep_size = '10GB'; -- Ensure logical replication is enabled SHOW wal_level; -- Should be 'logical'
Docker resource limits:
realtime:
deploy:
resources:
limits:
cpus: '2'
memory: 2G
reservations:
cpus: '0.5'
memory: 512M
Health checks:
realtime:
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:4000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
Monitoring Realtime in Production
Realtime exposes Prometheus metrics at the /metrics endpoint. Key metrics to track:
- Connection count (current WebSocket connections)
- Message throughput (messages per second)
- Channel count (active subscriptions)
- Replication lag (how far behind Postgres changes)
If you're using Grafana, Supabase provides a dashboard template on GitHub that includes Realtime visualizations. For a deeper dive into observability, check out our guide on monitoring self-hosted Supabase.
When to Skip Realtime Entirely
Not every self-hosted deployment needs Realtime running. If your application doesn't use:
- Live subscriptions to database changes
- Real-time presence features
- Client-to-client broadcast
You can remove Realtime from your Docker Compose entirely and save resources. The selective service deployment approach can significantly reduce your infrastructure requirements.
# In docker-compose.yml, comment out or remove: # - realtime service # - realtime dependencies in other services
Managing Realtime Configuration with Supascale
Manually configuring Realtime environment variables, monitoring replication slots, and troubleshooting WebSocket issues gets tedious across multiple projects. Supascale handles much of this operational overhead:
- Sensible default configurations for Realtime
- Integrated health monitoring
- Simplified environment variable management
- One-click deployment with proper Docker Compose settings
At $39.99 one-time for unlimited projects, it eliminates the need to become a Realtime expert just to run a few self-hosted Supabase instances.
Conclusion
Supabase Realtime is powerful, but self-hosting it requires understanding its architecture and limitations. The key takeaways:
- Configure replication settings properly to avoid WAL buildup
- Understand the authorization overhead of Postgres Changes
- Set resource limits appropriate for your usage patterns
- Monitor replication lag and connection counts
- Consider whether you actually need Realtime for your use case
The Elixir/Phoenix stack handles concurrent connections efficiently, but the real bottleneck is usually your Postgres database—especially with RLS-heavy workloads.
For developers who want Realtime working reliably without deep-diving into Elixir configurations, Supascale provides a managed layer that handles the operational complexity while keeping your data fully self-hosted.
