Configuring Supabase Realtime for Self-Hosted Instances

Complete guide to setting up, configuring, and troubleshooting Supabase Realtime on your self-hosted instance. WebSockets, scaling, and production tips.

Cover Image for Configuring Supabase Realtime for Self-Hosted Instances

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:

LimitDefault Value
Maximum channels per tenant100
Maximum concurrent users per channel200
Maximum events per second per tenant100
Maximum bytes per second per tenant100,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:

  1. High-frequency writes to tables with many subscribers can overload your database
  2. Complex RLS policies compound the performance impact
  3. 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:

  1. Add more memory to your Realtime container
  2. Set max_slot_wal_keep_size to limit lag accumulation
  3. 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:

  1. Configure replication settings properly to avoid WAL buildup
  2. Understand the authorization overhead of Postgres Changes
  3. Set resource limits appropriate for your usage patterns
  4. Monitor replication lag and connection counts
  5. 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.


Further Reading