VPNSmith
self-host-vpnINFO

WireGuard PersistentKeepalive: keep the tunnel reachable behind NAT (2026)

Your WireGuard peer goes silent after a minute behind NAT? Set PersistentKeepalive = 25 on the client. Here's what it does, why 25 seconds, and where to put it.

By Eric Gerard · Founder · VPNSmith — Self-host VPN & GDPR VPS specialist7 min readPhoto via Unsplash

Affiliate disclosure — This post contains Contabo affiliate links. If you grab a VPS through them, we earn a commission at no extra cost to you. Every value and behaviour below is documented from official WireGuard sources and written to be reproducible on your own machine.

Your WireGuard client connects, works for a minute, then goes quiet: you can reach the server from the client, but the server can no longer push anything back to you. SSH from the server side hangs, a LAN device behind the client becomes unreachable, the handshake silently goes stale. The fix is almost always one line on the client: PersistentKeepalive = 25. This guide explains exactly what that option does, why 25 seconds is the magic number, and which side of the tunnel it belongs on.

The short answer

If a peer sits behind NAT and needs to stay reachable, add this to the [Peer] block on that peer's config:

[Peer]
PublicKey = ...
Endpoint = server.example.com:51820
AllowedIPs = 0.0.0.0/0
PersistentKeepalive = 25

Restart the tunnel and the silent-tunnel-after-idle problem disappears. Everything below is the why.

What PersistentKeepalive does

By design, WireGuard is silent: when there is no traffic to send, it sends nothing. No keepalives, no heartbeats, no chatter. That makes it efficient and hard to fingerprint — but it breaks one specific scenario.

When a client is behind NAT (a home router, mobile carrier, office firewall), the NAT device creates a temporary mapping the first time the client sends a packet out. That mapping is what lets the server send replies back to the right internal address — think of it as a punched hole in the NAT. Crucially, that hole expires after a period of inactivity, often anywhere from 30 seconds to a couple of minutes for UDP. Once it closes, the server's packets have nowhere to go, and the peer becomes unreachable until the client speaks first again.

PersistentKeepalive solves this by sending a small, empty keepalive packet to the peer every N seconds. That tiny bit of traffic keeps the NAT mapping refreshed, so the hole never closes and the peer stays reachable even when idle.

Data-centre cabling and network equipment in a rack — the public-IP side of a self-hosted WireGuard tunnel
Data-centre cabling and network equipment in a rack — the public-IP side of a self-hosted WireGuard tunnel

Why 25 seconds

The official WireGuard recommendation is 25 seconds, and the reasoning is simple. Common UDP NAT timeouts cluster between 30 seconds and a few minutes, and the shortest value you regularly hit in the wild is around 30 seconds. Sending a keepalive every 25 seconds refreshes the mapping with a small margin before that 30-second floor — frequent enough to never let the hole close, infrequent enough to cost almost nothing.

You can go lower if your NAT is unusually aggressive (15 is a reasonable next step), or higher to save battery on mobile, but 25 is the documented default for a reason. Don't over-think it.

Where to put it: client vs server

This is the part people get wrong. The keepalive goes on the peer that needs to stay reachable through a NAT:

SetupKeepalive on client?Keepalive on server?
Roadwarrior client behind NAT → server on public IPYes (= 25)No
Server on a clean public IP(n/a)Not needed
Both ends behind NAT (peer-to-peer)YesYes

In the normal case — a laptop or phone behind a router talking to a VPS with a real public IP — only the client needs PersistentKeepalive = 25, in the [Peer] block that describes the server. The server doesn't need one toward its clients: the client always initiates, and the server just replies to whatever source address the last packet came from. A clean public-IP server is exactly the setup where you avoid the server-side NAT problem entirely.

Example [Peer] config

Here is a complete client-side peer block with the keepalive in place:

[Interface]
PrivateKey = <client-private-key>
Address = 10.66.66.2/32
DNS = 10.66.66.1

[Peer]
PublicKey = <server-public-key>
Endpoint = vpn.example.com:51820
AllowedIPs = 0.0.0.0/0, ::/0
PersistentKeepalive = 25

Apply it with a tunnel restart:

sudo wg-quick down wg0 && sudo wg-quick up wg0

Confirm it took effect — wg show lists the interval under the peer:

sudo wg show
# persistent keepalive: every 25 seconds

If you are assembling configs from scratch, the ready-to-use WireGuard config templates already include the keepalive line in the right block for each platform.

The "interval must be in range" error

PersistentKeepalive expects a plain whole number of seconds, validated against the range 0 to 65535. If you see interval must be in range or must be in range 0 to 65535, you passed something that isn't a valid integer in that window:

  • A unit suffix — write 25, not 25s.
  • A decimal — 25, not 25.0.
  • A negative number, or a value above 65535.
  • A stray space or invisible character pasted from a web page.

A value of 0 is legal and means disabled — the same as leaving the line out entirely. So PersistentKeepalive = 0 does nothing useful; use a real interval like 25.

Keepalive set but still not working

If you added the line and the tunnel still goes silent after idle, work through these honest causes in order:

  • Set on the wrong side. The peer that's behind NAT is the one that needs it. A keepalive on the public-IP server alone won't keep a client's NAT hole open.
  • NAT timeout shorter than your interval. Some carrier-grade NAT closes UDP mappings faster than 30 seconds. Drop the interval to 15 and re-test.
  • A firewall is blocking the path. If the underlying UDP port is filtered, no keepalive will get through — confirm the WireGuard port is actually open on both ends.
  • The endpoint changed (roaming). If the client moved networks and its public address changed, the server may still be aiming at the old Endpoint. The keepalive helps the server track the new source, but a stale client-side Endpoint can still misfire.

Keepalive vs the WireGuard handshake

These are two different timers, and it helps to separate them. WireGuard renews its cryptographic handshake roughly every two minutes — but only when there is actual traffic to protect. On an idle link with no keepalive, no traffic means no handshake renewal, and meanwhile the NAT hole quietly closes.

PersistentKeepalive fills exactly that gap: it keeps a trickle of traffic flowing on an otherwise idle link, holding the NAT mapping open between handshakes so the tunnel is ready the instant real data needs to move. The keepalive doesn't replace the handshake; it keeps the path alive so the handshake can happen when needed.

A public-IP server removes half the problem

Most keepalive headaches come from NAT on the server side — a VPN box behind a home router, double NAT, a provider firewall. Put the server on a clean public IPv4 and the only NAT left in the picture is the client's, where a single PersistentKeepalive = 25 is all you need. A Contabo Cloud VPS 10 at €5.50/month gives you full root and a real public IP with no provider firewall to fight — the exact conditions where the client-side keepalive is the only thing you have to set. To understand the NAT mechanics this option works around, see the plain-English explainer on what NAT is.

Going further

Sources and references:


Published 2026-06-29. The 25-second recommendation and the behaviour of PersistentKeepalive are from official WireGuard documentation; NAT timeout ranges vary by device, so confirm against your own router or carrier if a shorter interval is needed.

Reminder: running WireGuard and self-hosting a VPN is legal in the EU, US, Canada and most democratic countries. VPNSmith publishes this content for educational purposes.

★ Nuremberg GDPR datacenter · ✓ Dedicated IPv4 included · 200+ Mbps guaranteed

Self-host your VPN on your own VPS → ContaboFull root access · public IPv4 · pick your region

Frequently asked questions

What does PersistentKeepalive do in WireGuard?
PersistentKeepalive is an option in the [Peer] section of a WireGuard config that makes the interface send a small empty keepalive packet to that peer every N seconds, even when there is no real traffic. By default WireGuard is silent: it sends nothing on an idle link. That silence is a problem when a client sits behind NAT, because the NAT mapping — the hole that lets the server reach back to the client — expires after a short idle period. The periodic keepalive keeps that mapping open, so the peer stays reachable. You enable it with one line: PersistentKeepalive = 25.
Why is the recommended PersistentKeepalive value 25 seconds?
The official WireGuard recommendation is 25 seconds. UDP NAT mappings on home routers and carrier-grade NAT typically time out somewhere between 30 seconds and a couple of minutes, and the shortest common timeout is around 30 seconds. Sending a keepalive every 25 seconds keeps the mapping refreshed with a small safety margin before that 30-second floor. Lower values work too but waste a little bandwidth and battery; 25 is the documented sweet spot. If your specific NAT is more aggressive, dropping to 15 can help.
Should PersistentKeepalive go on the client or the server?
Put it on the peer that needs to stay reachable through a NAT — almost always the roadwarrior client (your laptop, phone or home box behind a router). The client adds PersistentKeepalive = 25 in the [Peer] block that describes the server. A server with a real public IP does not need a keepalive toward its clients, because the client always initiates and the server simply replies to wherever the last packet came from. If both ends are behind NAT, set it on both.
Why am I getting 'interval must be in range 0 to 65535'?
PersistentKeepalive takes a whole number of seconds, and WireGuard validates it against the range 0 to 65535. The error 'interval must be in range' or 'must be in range 0 to 65535' means you passed something outside that — a negative number, a decimal, a unit suffix like '25s', or a stray character. Use a plain integer: PersistentKeepalive = 25. A value of 0 (or leaving the line out) disables the feature entirely.
Does PersistentKeepalive drain my battery or data?
Bandwidth-wise it is negligible: a keepalive is a small empty packet of roughly 32 bytes sent every 25 seconds, a few kilobytes per hour. The real cost is on mobile, where every keepalive wakes the radio out of its low-power sleep, and frequent wakeups can shorten battery life. That is why some people raise the interval on phones to save energy, accepting that the tunnel may go briefly unreachable while idle. On a always-on desktop or server the cost is effectively zero.