Why Tailscale is slow: relay vs direct
Relayed Tailscale is slow, direct is close to line rate. Two commands tell you which you have, plus the blocked UDP and hard NAT fixes for a VPS.
Why Tailscale is slow: relayed instead of direct
Tailscale is slow when the connection is relayed, and it is close to line rate when the connection is direct. A direct connection carries encrypted WireGuard packets straight from one machine to the other, so it runs at whatever the two internet links can carry. A relayed connection sends every packet through a third machine first, so it inherits that machine's latency and whatever share of its bandwidth you get. Tailscale's own performance page states it in one line: "Direct connections nearly always result in lower latency and higher throughput."
Nothing inside your application shows the difference. The file copy is just slow, the SSH session is just laggy. So the first job is to find out which kind of connection you already have. Two commands answer that in under a minute, and everything after that is fixing the reason. The setup part is worth knowing before you start, because the coordination server and the WireGuard data plane are separate systems and only the data plane moves your bytes.
The two commands that tell direct from relayed
Send some traffic to the peer before you measure anything. Tailscale builds a path on demand, so a peer you have not talked to today may not have negotiated one yet, and you would be reading a stale answer. One ping or one curl to the peer's tailnet address is enough.
tailscale statusThe answer is at the end of each peer's line.
100.113.160.82 device-a tagged-devices linux active; offers exit node; direct 203.0.113.9:41641
100.104.93.78 device-b you@ android active; relay "tor"direct followed by an address and a port means packets are going straight to that address. relay "tor" names a DERP server (designated encrypted relay for packets), which is one of Tailscale's relay machines, and every packet to that peer is passing through it. A third value, peer-relay, is covered in the next section.
tailscale ping device-bA healthy connection starts relayed and then moves. The first packets go through the nearest DERP server while the two machines negotiate, and then the path changes under you:
pong from device-b (100.113.160.82) via DERP(tor) in 51ms
pong from device-b (100.113.160.82) via DERP(tor) in 48ms
pong from device-b (100.113.160.82) via 203.0.113.9:41641 in 35msThe run stops there because --until-direct defaults to true. A connection that will not go direct looks like this instead, and it ends with a sentence rather than a pong:
pong from device-b (100.104.93.78) via DERP(tor) in 53ms
pong from device-b (100.104.93.78) via DERP(tor) in 60ms
direct connection not establishedThat last line is the finding. It means Tailscale sent every probe it was going to send and never got a direct path. To keep watching a relayed path instead of stopping at the first direct one, run tailscale ping --until-direct=false -c 20 device-b and read the latency spread. A relayed path usually shows both higher numbers and more variation, because it is two internet paths joined at a machine you do not control.
What does peer-relay mean in tailscale status?
A peer relay is a machine in your own tailnet that relays traffic for other members when a direct connection is impossible. It listens on a UDP port you choose, and the daemon uses it in preference to DERP. tailscale status marks such a connection peer-relay, and tailscale ping prints the relay's endpoint:
pong from device-b (100.97.143.93) via peer-relay(203.0.113.42:40000:vni:1) in 4ms
direct connection not establishedRead that carefully. It is still not a direct connection, so the run still ends with direct connection not established. What changed is who is relaying. A VPS with a public IP address and a generous bandwidth allowance is a much better relay for your own traffic than a shared DERP node, which is why this matters to anyone renting a server. Enable it on the machine that has the clean public endpoint:
sudo tailscale set --relay-server-port=40000A port of 0 picks a random unused port, and an empty string disables the relay server. Then grant the client devices permission to use it, with the tailscale.com/cap/relay capability in your tailnet policy file:
{
"grants": [
{
"src": ["tag:us-east-vpc"],
"dst": ["tag:us-east-relays"],
"app": {
"tailscale.com/cap/relay": []
}
}
]
}Both the relay device and the client devices need Tailscale 1.86 or later, so check with tailscale version on each before you spend an hour on the policy file. The order the daemon tries is worth memorising. It attempts a direct connection first. If that fails it looks for a peer relay it is allowed to use. If there is none, it falls back to DERP. DERP is never fully out of the picture, because it is also the channel over which the two machines negotiate in the first place.
Cause 1: an egress firewall that blocks UDP
Tailscale documents two reasons a connection stays relayed, and the first is blocked UDP. Ask the machine directly:
tailscale netcheckThe report is trimmed here, and the top field is the one that decides everything:
Report:
* UDP: true
* IPv4: yes, 203.0.113.9:41641
* IPv6: no
* MappingVariesByDestIP: false
* PortMapping:
* Nearest DERP: DallasUDP: false is the whole answer when you see it. The machine cannot get a UDP packet out to Tailscale's probe servers, so no direct path can form, and the daemon falls back to DERP over TCP port 443. That fallback is why the machine still looks perfectly healthy: it is connected, it is reachable, and every byte is being relayed.
Two outbound rules are documented. "Let your internal devices start UDP from :41641 to *:*", which is the WireGuard traffic itself, and "Let your internal devices start UDP to *:3478", which is STUN (session traversal utilities for NAT), the protocol the machine uses to learn its own public address and port. Use wildcards for the destinations. Tailscale adds relay servers over time, and a hand written list of addresses will be wrong within a year.
On a rented server the usual culprit is an aggressive egress policy, either one you inherited in a hardened image or one your provider applies upstream. Look at the default outgoing policy first:
sudo ufw status verbose
sudo nft list rulesetDefault: deny (incoming), allow (outgoing) is fine and is not your problem. An outgoing default of deny with a short allow list of TCP 443 and DNS is exactly the shape that keeps a server on a relay forever, because the DERP path over TCP 443 fits through that hole and the direct path does not. Where those rules actually live depends on whether the box is running iptables or nftables underneath, and editing the wrong one is a common way to change nothing.
The inbound side matters too, because a VPS has a public IP address and can therefore be the easy half of the pair. If its firewall accepts inbound UDP on the port tailscaled listens on, peers behind awkward home routers can reach it without any tricks. Find the port that is actually in use:
sudo ss -lunp | grep tailscaled
sudo ufw allow 41641/udp41641 is the default static port. A tailnet with the randomizeClientPort setting turned on makes clients pick a random port instead, in which case take the real number from the ss output rather than from this page. Then check your provider's control panel. Most hosts run a network firewall that is separate from the one inside the server, and a rule you added with ufw does nothing about it.
Cause 2: hard NAT on one or both ends
The second documented cause is hard NAT. NAT (network address translation) is what a router does when it rewrites your private address into its public one. A friendly router keeps the same public port for a given internal socket no matter who you are talking to, which is called endpoint independent mapping. A hard NAT gives out a different public port per destination, so the address the machine learned from a STUN server is not the address a peer will be able to use. Tailscale reports this in netcheck as MappingVariesByDestIP: true.
One hard NAT is survivable. If the other side has a stable public endpoint, the machine behind the hard NAT can still reach in and the path forms. Two hard NATs at once is the case that fails, because neither side can predict the port the other will appear on.
On a VPS with a public IPv4 address this field should read false, since nothing is translating that address. If it reads true on a server you rent, the address is being translated somewhere in the provider's network, and no firewall rule inside the machine will change that. Your options are to put a peer relay on a machine that does have a clean public endpoint, or to move the workload. This is also the case where advertising your private ranges from a subnet router pays off, because you then need one good path into the network rather than a good path to every device in it.
Why an exit node makes Tailscale look slower than it is
An exit node is a second hop, and readers blame the tunnel for it. With an exit node selected, a request leaves your laptop, crosses the tunnel to the VPS, leaves the VPS to the public internet, and the reply comes back the same way. Even a perfectly direct connection to that VPS cannot make the total faster than the VPS's own uplink, and the added distance shows up in every page load.
Measure the two halves separately. Turn the exit node off, then test the tunnel on its own against the VPS's tailnet address:
sudo tailscale set --exit-node=
sudo apt install -y iperf3
iperf3 -sRun iperf3 -c 100.113.160.82 from the client against that tailnet address. That number is your tunnel. Now turn the exit node back on with sudo tailscale set --exit-node=100.113.160.82 and run a normal speed test to the public internet. That number is your tunnel plus the VPS's uplink. If the first number is good and the second is bad, Tailscale is not the problem and the exit node's own network and sizing is where to look. tailscale exit-node list shows what is on offer if you are unsure which node you selected.
CPU is the other half of an exit node's ceiling. Tailscale's guidance is to prefer a recent CPU generation with a higher clock speed over a larger core count, so a plan with more vCPUs is not automatically faster here. On a busy shared host the CPU you were promised is not the CPU you get, and steal time from a noisy neighbour shows up as throughput that varies by the hour with no change on your side.
The one tuning knob: rx-udp-gro-forwarding
Tailscale documents a single Linux setting, and it applies to machines that forward traffic, meaning exit nodes and subnet routers. A plain client does not benefit. It needs Tailscale 1.54 or later and Linux kernel 6.2 or later, so confirm both before you change anything:
tailscale version
uname -rWith those in place, turn on UDP GRO (generic receive offload) forwarding on the interface that faces the internet:
NETDEV=$(ip -o route get 8.8.8.8 | cut -f 5 -d " ")
sudo ethtool -K $NETDEV rx-udp-gro-forwarding on rx-gro-list offCheck it took:
ethtool -k $NETDEV | grep -E 'rx-udp-gro-forwarding|rx-gro-list'You should see rx-udp-gro-forwarding: on and rx-gro-list: off. The reason this helps is that Tailscale's traffic is UDP, and letting the kernel keep small UDP packets coalesced across the forwarding path means the daemon handles fewer, larger segments for the same number of bytes. ethtool -K does not survive a reboot, so persist it. On a system using networkd-dispatcher:
printf '#!/bin/sh\n\nethtool -K %s rx-udp-gro-forwarding on rx-gro-list off \n' "$(ip -o route get 8.8.8.8 | cut -f 5 -d " ")" | sudo tee /etc/networkd-dispatcher/routable.d/50-tailscale
sudo chmod 755 /etc/networkd-dispatcher/routable.d/50-tailscaleRun the script once by hand and check its exit status is 0. A forwarding node also needs IP forwarding enabled in the first place, which is a separate setting and a separate failure:
echo 'net.ipv4.ip_forward = 1' | sudo tee -a /etc/sysctl.d/99-tailscale.conf
echo 'net.ipv6.conf.all.forwarding = 1' | sudo tee -a /etc/sysctl.d/99-tailscale.conf
sudo sysctl -p /etc/sysctl.d/99-tailscale.confWhat MTU is your tailscale0 interface using?
Stop guessing at this number and read it off the machine:
ip link show tailscale0The mtu value in that output is what your tunnel actually uses, and it is lower than the 1500 your ethernet interface reports. That is deliberate, not a bug. Every packet you send inside the tunnel is wrapped: an outer IP header of 20 bytes for IPv4 or 40 for IPv6, a UDP header of 8 bytes, and the WireGuard framing and authentication tag of 32 bytes. All of that has to fit inside whatever the real path can carry. Tailscale then picks a value low enough to survive links that carry less than a full 1500 byte packet, which includes PPPoE connections, some mobile networks, and IPv6 tunnels.
The symptom of an MTU problem is specific, so do not diagnose it from slowness alone. SSH is responsive, ping works, and then large transfers or big HTTPS pages stall completely rather than running slowly. That pattern means oversized packets are being dropped somewhere with no ICMP message getting back to tell the sender. Raising the tailscale0 MTU toward 1500 makes it worse, since the packets that already do not fit get larger. The real fix is to find the working path MTU by bisection and clamp the TCP MSS on the router that forwards the traffic. The connection type does not change any of this: a relayed path and a direct path use the same interface MTU.
FAQ
How do I know if my Tailscale connection is direct or relayed?
Run tailscale status and read the end of the peer's line. direct 203.0.113.9:41641 is a direct connection, relay "tor" means every packet goes through that DERP server, and peer-relay means the packets go through a machine in your own tailnet. For a second opinion run tailscale ping <peer>: a healthy path starts on DERP and then prints a pong with a plain address and port, while a relayed path prints DERP pongs until the run ends with direct connection not established. Send some traffic to the peer first, because Tailscale only builds a path on demand.
Why does my VPS never get a direct connection?
Run tailscale netcheck on the VPS. If it prints UDP: false, an egress firewall is dropping outbound UDP and the daemon has fallen back to DERP over TCP 443, which is why the machine still looks connected. Allow outbound UDP from port 41641 to any destination and outbound UDP to any destination on port 3478. Check your provider's network firewall as well as the one inside the server, because they are separate controls and a ufw rule does not touch the provider's.
Is a relayed Tailscale connection less secure than a direct one?
No. A DERP server forwards WireGuard packets it cannot decrypt, because the encryption keys are generated on your devices and never leave them. The cost of a relay is latency and throughput, not confidentiality. What the coordination server does control is which devices learn about each other, and the split between key material and connection metadata is worth understanding before you decide how much of it to self host.
Does the rx-udp-gro-forwarding setting help every machine?
No. It is documented for Linux machines that forward traffic for others, which means exit nodes and subnet routers. A laptop or a server that only talks to its own peers gains nothing from it. It also requires Tailscale 1.54 or later and Linux kernel 6.2 or later, so check tailscale version and uname -r first, and remember that ethtool -K resets on reboot unless you persist it.
Is Tailscale slower than plain WireGuard?
Both use WireGuard to encrypt your traffic. Tailscale adds the connection setup that plain WireGuard leaves you to do by hand, and that setup is what sometimes lands you on a relay. Plain WireGuard has no relay to land on: it connects directly or it fails to connect at all. So compare like with like, and only benchmark Tailscale when tailscale status says direct. If you want the hand configured version for comparison, a WireGuard server you build yourself takes about forty lines of config, and the trade you are making is covered in the comparison of the two approaches.