Build your own IRL streaming relay, on a VPS or at home

Last updated: August 2026. Written by the team behind Enhanced IRL — including the parts that make self-hosting a bad idea for some people, and the parts where it beats paying us. Versions referenced: MediaMTX v1.20.1, NOALBS v2.19.1.

A relay is the server that sits between your phone or encoder and Twitch. It takes an unstable mobile connection, absorbs the jitter and hands a clean stream to the platform. You do not need a company to run one for you: the software is open source and a small VPS is enough.

This is the honest version of how to do it yourself: the actual commands, the actual configuration files and what breaks at each step. If at the end you decide it is not worth your time, that is a legitimate outcome too — but you should decide it knowing what the work actually is, not because someone hid it from you.

What you need

  • A VPS, not a big one. A pure relay remuxes packets, it does not re-encode video, so it barely uses CPU. 2 vCPU and 2 GB of RAM handle several streams. Do not buy a GPU box for this — that is a different problem, see below.
  • A location close to you. Latency to the relay is the part you control. A server in the country you stream from beats a cheaper one three borders away, every time.
  • Generous egress. Your bytes go in once and out again. At 6 Mbps that is roughly 2.7 GB per hour in each direction. 100 hours a month is around 270 GB out — inside most VPS allowances, but check yours before you get a surprise invoice.
  • MediaMTX. One open-source binary that speaks RTMP and SRT, both for ingest and for handing the stream on. It is the core of the setup and the easy part.
  • An SRTLA receiver, only if you bond. If you bond several modems with a BELABOX or Moblin, you also need something that speaks SRTLA and un-bonds it into plain SRT. BELABOX's srtla_rec, or a maintained fork of it, does that. This is the fiddly part.
  • NOALBS, and an OBS to point it at. Automatic scene switching is not part of the relay. It is a separate open-source tool that watches your bitrate and drives OBS. Without it, a bad connection simply shows viewers a frozen picture until you get back to a keyboard.
  • A way to know it broke. Uptime monitoring on the ingest port. Without it you find out you are down because chat tells you.

How it fits together

The shape of the system, in the order you build it. The rest of this page is each step in full:

  1. Install MediaMTX on the server. Run it as a service so it comes back after a reboot. Its config file is a single YAML: you enable the RTMP and SRT listeners and define a path for your stream.
  2. Open the ports, and mind the protocol. RTMP is TCP 1935. SRT is UDP 8890 by default. SRTLA is UDP too, 5000 by default. The single most common failure is a firewall or a provider that quietly drops UDP — if SRT will not connect but RTMP does, look there first.
  3. Put authentication in front of it. An open ingest is an open invitation. MediaMTX can check credentials from its config or delegate to an HTTP endpoint you control. Do not skip this and do not rely on the stream name being hard to guess.
  4. Add SRTLA if you bond. The receiver listens on its own UDP port, re-assembles the bonded links and forwards plain SRT into MediaMTX, preserving the stream id. Your encoder then points at an srtla:// URL instead of srt://.
  5. Decide what consumes the stream. Either your OBS at home pulls from the relay and publishes to Twitch, or the relay republishes to Twitch directly. The first gives you scenes and overlays; the second is simpler but you get whatever the camera sends, with no production on top.
  6. Add NOALBS so a bad connection is not a dead stream. It polls the relay for your current bitrate and switches OBS scenes when it drops. This is the step that separates a relay that works at your desk from one that works on the street.
  7. Tune the SRT latency. The latency parameter is the buffer that hides packet loss. Too low and every dropped packet becomes a visible glitch; too high and you add delay for no reason. A common starting point is two to four times your round-trip time, then adjust against a real connection, not a test on your desk.

VPS or your own machine at home

The software is identical either way. What changes is the network, and the network is the whole thing:

  • On a VPS you get a public IP. Your encoder can reach it from anywhere with a SIM card, the routing is a data centre's problem, and the machine does not reboot because someone unplugged something.
  • At home you probably do not. Most home connections sit behind NAT, so you need port forwarding on the router — TCP 1935, UDP 8890, and UDP 5000 if you bond — plus a dynamic DNS name, because your address changes.
  • CGNAT ends the conversation. Many fibre connections and all mobile ones put you behind carrier-grade NAT, where no inbound connection reaches you at all and no router setting fixes it. Ask your ISP for a public IPv4 address; if the answer is no, a relay at home is not an option.
  • Your home upload becomes the ceiling. A stream coming in from the street arrives on your download and leaves again on your upload, sharing that line with everything else in the house. A data centre gives you symmetric bandwidth; a home line often gives you 30 Mbps up, minus whoever else is home.
  • You are publishing your home IP address. Anyone who learns it knows where your house sits on the internet, and a residential line has none of the filtering a provider puts in front of an attack.
  • Local is genuinely good for two things. Learning the whole stack without paying for a month of server, and streaming where the encoder is on the same LAN as the relay — a fixed setup at home, gameplay, a second camera in the next room.

Where a local relay does not work is the case people most often want it for: streaming from the street back to your own house. You are pointing an unstable mobile connection at a residential line with worse routing, no redundancy, a router that reboots when it feels like it, and one power cut away from ending your stream — while you are outside and cannot touch any of it. Build it locally to learn it, then move the relay somewhere with a public IP before it matters.

Installing MediaMTX

MediaMTX ships as a single static binary with no dependencies. Download the build for your architecture, put the binary somewhere on the path and the config where the binary looks for it. This is Debian or Ubuntu on x86-64; adjust the file name for arm64.

# Latest release at the time of writing. Check for a newer one before copying this.
curl -L -o mediamtx.tar.gz \
  https://github.com/bluenviron/mediamtx/releases/download/v1.20.1/mediamtx_v1.20.1_linux_amd64.tar.gz
tar -xzf mediamtx.tar.gz

sudo install -m 755 mediamtx /usr/local/bin/mediamtx
sudo mkdir -p /etc/mediamtx
sudo mv mediamtx.yml /etc/mediamtx/mediamtx.yml

Running the binary from an SSH session is fine for a first test and useless afterwards: it dies when you close the terminal and it does not come back after a reboot. Give it a systemd unit in /etc/systemd/system/mediamtx.service.

[Unit]
Description=MediaMTX
After=network-online.target
Wants=network-online.target

[Service]
# Every port it binds is above 1024, so it does not need root.
User=mediamtx
Group=mediamtx
ExecStart=/usr/local/bin/mediamtx /etc/mediamtx/mediamtx.yml
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
# The unprivileged account the service runs as.
sudo useradd --system --no-create-home --shell /usr/sbin/nologin mediamtx

sudo systemctl daemon-reload
sudo systemctl enable --now mediamtx
sudo systemctl status mediamtx

MediaMTX searches a list of locations for its config file, /etc/mediamtx/mediamtx.yml among them, and the unit above passes the path explicitly anyway. The file that ships with the release is long because it documents every option; nearly all of it is defaults you never touch. This is the part that matters for a relay:

logLevel: info

# Turn off what you are not going to use. Fewer open ports, fewer problems.
rtsp: false
hls: false
webrtc: false
moq: false

# Ingest 1: RTMP, over TCP.
rtmp: true
rtmpAddress: :1935

# Ingest 2: SRT, over UDP.
srt: true
srtAddress: :8890

# Stats API, bound to localhost. NOALBS reads the bitrate from here.
api: true
apiAddress: 127.0.0.1:9997

# Replacing this list removes the default anonymous user, which can publish
# and read anything. That default is fine on a laptop and wrong on a server.
authInternalUsers:
  # The account your phone or encoder publishes with.
  - user: irl
    pass: a-long-random-password
    permissions:
      - action: publish
        path: live
  # The account your OBS reads with.
  - user: obs
    pass: another-long-random-password
    permissions:
      - action: read
        path: live
  # Local administrator: lets NOALBS query the API, from this machine only.
  - user: any
    pass:
    ips: ['127.0.0.1', '::1']
    permissions:
      - action: api

paths:
  # One named path. The stream lives at "live" and nowhere else.
  live:
    source: publisher

Two things there are easy to get wrong. The path under each permission is what stops a user publishing under any name they invent — leave it empty and the restriction is gone. And there is no default password to change: replacing authInternalUsers is the entire security step, so use passwords you generated rather than ones you typed.

Open the ports on the firewall, then confirm the service is up and the ports are really listening before you touch anything else:

# The two ingest ports. RTMP is TCP, SRT is UDP — the protocol is not optional here.
sudo ufw allow 1935/tcp
sudo ufw allow 8890/udp

# Is the service running, and did it survive the config file?
systemctl status mediamtx
journalctl -u mediamtx -f

# Are the ports actually bound?
ss -lntup | grep -E '1935|8890'

# Does the API answer locally?
curl -s http://127.0.0.1:9997/v3/paths/list

What goes wrong at this step:

  • The service will not start. Nine times out of ten it is the YAML, and journalctl -u mediamtx names the offending key. Tabs instead of spaces, and a missing space after a colon, are the usual culprits.
  • It listens on the wrong address. srtAddress: :8890 listens on every interface; 127.0.0.1:8890 listens only on the machine itself, and nothing from outside will ever reach it. Easy to do by accident, invisible until you test remotely.
  • The firewall on the machine is not the only firewall. Most cloud providers put a security group or network firewall in front of the instance as well. Open UDP there too, or ufw will look correct while packets die one hop earlier.
  • The API answers but nothing else does. That is the expected state before the ports are reachable from outside. It is not a MediaMTX problem.

Publish to it, and read it back

The credentials travel differently in each protocol: RTMP passes them as query parameters, SRT carries them inside the stream id. Replace SERVER with your host name or address, and the passwords with the ones you actually set.

# RTMP. Encoders that split "server" and "stream key" take rtmp://SERVER:1935/
# as the server and live?user=irl&pass=SECRET as the key.
rtmp://SERVER:1935/live?user=irl&pass=SECRET

# SRT.
srt://SERVER:8890?streamid=publish:live:irl:SECRET&pkt_size=1316

# A test broadcast from any PC, no phone involved. If this arrives,
# the server half of the setup is finished.
ffmpeg -re -f lavfi -i testsrc2=size=1280x720:rate=30 -f lavfi -i sine \
  -c:v libx264 -preset veryfast -b:v 3000k -g 60 -c:a aac -b:a 128k \
  -f mpegts "srt://SERVER:8890?streamid=publish:live:irl:SECRET&pkt_size=1316"

While that is running the path becomes ready. Check it from the server, then add the read URL to OBS as a Media Source with 'Local file' unchecked:

# ready:true and a rising bytesReceived means the stream is arriving and the
# credentials were accepted.
curl -s http://127.0.0.1:9997/v3/paths/get/live

# URL for the OBS media source. 2000000 microseconds = 2 seconds of buffer.
srt://SERVER:8890?streamid=read:live:obs:SECRET&latency=2000000

The latency parameter in an OBS or ffmpeg URL is expressed in microseconds, while phone apps ask for it in milliseconds. Typing 2000 in OBS gives you two milliseconds of buffer and a picture that falls apart on the first lost packet. Start at two to four times your round-trip time to the server, then adjust against a real mobile connection.

SRTLA, only if you bond modems

SRTLA is the BELABOX protocol that splits one SRT stream across several modems and puts it back together at the far end. MediaMTX does not speak it, so you put a receiver in front: it listens on its own UDP port, reassembles the links and forwards plain SRT to MediaMTX on localhost. It is a transport proxy, so the SRT handshake — stream id and credentials included — passes through untouched and a bonded stream authenticates exactly like a direct one. Note that the receiver in the original BELABOX repository is documented by its own authors as unsupported and not intended for production; the maintained fork below is what most people run.

# The maintained fork. It builds with CMake and needs a C++11 compiler
# plus the spdlog and argparse libraries.
git clone https://github.com/OpenIRL/srtla.git
cd srtla && mkdir build && cd build
cmake .. && make

Point it at the SRT port MediaMTX is already listening on. Give it its own systemd unit for the same reason you gave MediaMTX one — the shape of that unit is identical, only the ExecStart line changes.

# Listen for bonded links on UDP 5000, hand plain SRT to MediaMTX on localhost.
./srtla_rec --srtla_port 5000 --srt_hostname 127.0.0.1 --srt_port 8890

sudo ufw allow 5000/udp

# In Moblin or BELABOX the URL scheme and port change. The stream id does not:
#   URL:       srtla://SERVER:5000
#   stream id: publish:live:irl:SECRET

What goes wrong here:

  • The encoder still points at srt://. Different protocol, different port. An SRT client talking to the SRTLA port gets nowhere, and the error it shows usually reads like an authentication failure, which sends you hunting in the wrong file.
  • Only one modem contributes. Bonding needs the sender to route each socket out of a specific interface. If the phone sends everything down one link, SRTLA aggregates nothing — and nothing in the logs says so.
  • Moblin's 'Big packets' option is on. With it enabled the connection frequently will not establish at all. Turn it off; the symptom looks like a rejected stream key.
  • MediaMTX now sees every stream coming from 127.0.0.1. Expected — the receiver is the client from MediaMTX's point of view. It also means an ips: allow-list on the publishing user no longer filters anything useful.

NOALBS: automatic scene switching

A relay keeps the packets moving. It does nothing about what viewers see when your connection collapses to 300 kbps in a lift. That job belongs to NOALBS, an open-source project that polls your ingest server for the current bitrate and tells OBS to switch scenes when it drops. It is the piece that turns a relay into something you can actually stream through.

It runs wherever OBS runs — your PC, not the server — because it drives OBS over its WebSocket. It is a single binary with a config.json and an optional .env beside it, released for Windows, macOS and Linux. Version 2 talks to MediaMTX natively, so there is no nginx and no stats page to build.

The scenes you need in OBS

NOALBS switches between scenes by name, and the names in the config must match the names in OBS character for character. This is the single most common reason a setup that looks correct does nothing at all:

  • A normal scene. Your actual stream: the relay feed full screen. Anything above the low threshold puts you here.
  • A low scene. The same feed, framed for a picture that has gone soft — smaller, cropped, with an overlay saying the connection is struggling. The stream keeps running and nobody has to guess what happened.
  • An offline scene. No feed at all. A card, a holding loop, music. This is what viewers get the moment the source disconnects, and it is the difference between a rough patch and a dead stream.
  • Optional extras. Starting, ending, privacy and refresh scenes get their own chat commands if you configure them. None of them are needed for bitrate switching to work.

Wiring OBS

In OBS, Tools → WebSocket Server Settings: enable the server, set a password and note the port, 4455 by default. OBS 28 and newer ship WebSocket v5, which is what NOALBS v2 expects. If OBS and NOALBS run on the same machine, leave that server on localhost and do not expose the port to anything.

Letting NOALBS see the relay

NOALBS needs to read the MediaMTX API, and the config above deliberately bound that API to localhost on the server. Do not undo that: forward the port over SSH from the machine running OBS instead. The API stays unreachable from the internet and NOALBS talks to it as though it were local.

# Run this on the PC with OBS, and leave it running while you stream.
ssh -N -L 9997:127.0.0.1:9997 you@SERVER

config.json

A minimal config for one MediaMTX server and no chat integration. Chat is optional: set it to null and NOALBS still switches scenes, it just will not answer commands in your Twitch chat. If you do want that, the project's README walks through generating a bot token and filling in the .env file.

{
  "user": { "id": null, "name": "yourname", "passwordHash": null },
  "switcher": {
    "bitrateSwitcherEnabled": true,
    "onlySwitchWhenStreaming": true,
    "instantlySwitchOnRecover": true,
    "autoSwitchNotification": false,
    "retryAttempts": 5,
    "triggers": { "low": 800, "rtt": 2500, "offline": 300, "rttOffline": 4000 },
    "switchingScenes": { "normal": "LIVE", "low": "LOW", "offline": "BRB" },
    "streamServers": [
      {
        "streamServer": {
          "type": "Mediamtx",
          "statsUrl": "http://127.0.0.1:9997/v3/paths/get/live"
        },
        "name": "relay",
        "priority": 0,
        "overrideScenes": null,
        "dependsOn": null,
        "enabled": true
      }
    ]
  },
  "software": {
    "type": "Obs",
    "host": "localhost",
    "port": 4455,
    "password": "your-obs-websocket-password"
  },
  "chat": null,
  "optionalScenes": {},
  "optionalOptions": {}
}

Things worth knowing before you start tuning the numbers:

  • The thresholds are yours to find. low, offline and the RTT triggers depend on the bitrate you actually stream at and on how your connection fails. The values above are a starting point, not a recommendation. Adjust them on a real walk, not at your desk.
  • retryAttempts is roughly seconds. NOALBS polls once per second, so 5 means about five consecutive bad readings before it switches. Lower it and every tunnel flips your scene; raise it and viewers watch a frozen picture first.
  • RTT triggers only do something over SRT. With a MediaMTX server, NOALBS reads round-trip time from the SRT connection statistics. Publish over RTMP and there is no RTT at all — only the bitrate triggers have any effect.
  • The path name is inside statsUrl. That URL ends with the path you defined in MediaMTX. Point it at a path that does not exist and NOALBS will see an offline stream forever, with no error to explain why.
  • onlySwitchWhenStreaming avoids surprises. With it on, NOALBS leaves your scenes alone until OBS is actually streaming — useful while you are still building the layout.

Check each piece before adding the next

Every failure in this stack looks identical from the outside: no picture. Building it in order and confirming each layer is what keeps a debugging session down to minutes instead of an evening:

  1. MediaMTX is alive. systemctl status says active and journalctl shows no config error. If this fails, nothing after it can possibly work.
  2. The ports answer from outside. From another machine, nc -zv SERVER 1935 covers TCP. UDP cannot be tested that way — publish a real SRT test stream and watch the server log instead.
  3. A test stream arrives. Run the ffmpeg command, then curl the API path. ready:true with a rising bytesReceived proves the ingest and the credentials both work.
  4. Your phone arrives too. Only now try Moblin, BELABOX or IRL Pro. If ffmpeg worked and the phone does not, the problem is in the encoder settings or the mobile network, not on the server.
  5. OBS pulls the feed. Add the read URL as a media source. A picture in OBS means the whole path works end to end.
  6. NOALBS reacts. Start it and watch its log: it should report a bitrate every second. Then kill the test stream and confirm OBS moves to the offline scene on its own. If nothing happens, the scene names do not match.

What it actually costs

A VPS able to do this runs somewhere between €5 and €20 a month depending on provider and region, and that is the whole bill if your egress stays inside the allowance. On paper it is cheaper than any managed service, and if you stream a lot of hours it stays cheaper.

The cost that does not appear on the invoice is that the server bills you for 24 hours a day whether you stream four hours a week or forty, and that you are now the person who fixes it. If the relay falls over mid-stream, there is no support line: there is you, on your phone, outside, with the stream down.

The parts that are genuinely hard

  • SRTLA, not SRT. A plain SRT or RTMP relay is a weekend project. Bonding is where the time goes: building the receiver, keeping it running and debugging why one modem contributes nothing are all real work.
  • UDP across the internet. Some hosting setups mangle or drop UDP, and some cloud features break it in ways that are not documented. Reserved or floating IPs are a classic offender. When SRT fails and RTMP works, suspect the network before the software.
  • There is no failover. One server is one point of failure. If it reboots for maintenance while you are live, your stream ends. Solving that properly means a second machine and something that keeps the platform connection alive — which is a whole project of its own.
  • Security is now yours. Public ingest ports, open-source binaries to keep patched and a Linux box on the internet. None of it is exotic, but it is a recurring chore that never announces itself.
  • It does not give you cloud OBS. A relay moves video. Running OBS itself in the cloud needs a GPU machine, a desktop session and a remote way to drive it, and rented GPUs cost several times what this VPS costs. If what you want is to stream without a PC at home, self-hosting a relay does not get you there.

When to self-host and when not to

There is no universal answer. Roughly:

  • Self-host if you already run servers. If a systemd unit and a firewall rule are a normal afternoon for you and you enjoy that, you will end up with exactly what you want and pay less for it.
  • Self-host if you stream a lot of hours. The economics improve the more you use the machine you are renting around the clock.
  • Self-host if you need an unusual region. If you stream from somewhere no provider covers well, a VPS near you beats a distant managed relay no matter how good the service is.
  • Do not self-host if you stream a few hours a week. You pay for a full month of server to use a handful of hours, and you take on the maintenance for the privilege.
  • Do not self-host if you need bonding and do not enjoy this. SRTLA is the part that will cost you evenings. If bonding is essential to your stream, be honest about whether you want that to be your problem.
  • Do not self-host if you want to stop debugging at 2am. The real product a managed relay sells is not the server. It is not being on call for your own stream.

Still deciding whether to run any of this yourself? The three routes, priced side by side: IRL streaming server: what it is and which one you need.

Frequently asked questions

Is self-hosting an IRL relay actually cheaper?

In pure euros, usually yes: a VPS runs €5–20 a month, well under what any managed relay or cloud OBS costs. The comparison stops being obvious once you count your own time, the hours the server sits idle being billed, and the streams you lose while you debug.

What is NOALBS and do I need it?

NOALBS is an open-source tool that reads the current bitrate from your ingest server and switches OBS scenes when it drops or the source disconnects. You need it, or something like it, for IRL: without it a bad connection shows viewers a frozen picture until you get back to a keyboard. It is separate from the relay, it runs on the machine with OBS, and it speaks to MediaMTX natively in version 2.

Can I run the relay at home instead of paying for a VPS?

You can, and it is a good way to learn the stack. It stops working when you are the one out on the street: you need a public IP, which CGNAT often makes impossible, you need port forwarding and dynamic DNS, and your home upload becomes the ceiling for the stream going out to Twitch. A power cut or a router reboot ends your stream while you are somewhere you cannot fix it.

Why does SRT fail when RTMP works?

Almost always UDP. SRT and SRTLA run over UDP, RTMP over TCP, so a firewall rule or a provider network feature that drops UDP breaks one and not the other. Check the machine's firewall, then the provider's security group, then any reserved or floating IP feature you have enabled.

Can I run OBS on the same VPS?

Not usefully. OBS needs a GPU to encode a real scene, plus a desktop session and a way to control it remotely. That is a different and considerably more expensive machine than a relay.

What happens if the server dies while I am live?

The stream ends. A single server has no failover, and building one that keeps the platform connection alive through an outage is a substantial project in itself.

The projects this is built on

Useful even if you self-host

These guides are part of our product documentation, but the protocol, encoder and scene-switching parts apply to any relay, ours or yours:

Keep reading

Or skip the server

A relay stream key with RTMP, SRT and SRTLA ingest in Europe, Asia and the US is €5/month, billed monthly, cancel anytime. If you would rather build it yourself, the guide above is genuinely all of it.

Get started