Blog

PersistentKeepalive: when WireGuard needs it

What PersistentKeepalive does, which peer sets it, why 25 seconds, and a lab run that shows inbound packets dying behind a NAT without it and arriving with it.

Published October 29, 2026 · 9 minutes read

Key takeaways

  • PersistentKeepalive is off by default; a value of 25 makes a peer send an empty authenticated packet every 25 seconds.
  • Only a peer behind NAT or a stateful firewall that must receive while idle needs it; a peer with a public address needs none.
  • In the lab on 2026-10-29, inbound pings after 70 idle seconds failed in both runs without it and arrived in all three with it.
  • The keepalive renews the NAT mapping, not the handshake: latest handshake kept ageing in every keepalive run.
On this page (7)

PersistentKeepalive makes a WireGuard peer send an empty packet at a fixed interval, so the NAT in front of it keeps the mapping that lets replies in. Set it, usually to 25 seconds, on a peer behind NAT that must receive while idle; a peer with a public address needs none.

NAT mapping
The entry a NAT or stateful firewall keeps for a UDP flow. Outgoing packets create or renew it; packets from outside are delivered only while it exists.

What does PersistentKeepalive do in WireGuard?

It sends an authenticated empty packet to the peer every set number of seconds, for one purpose: to keep a NAT or firewall mapping valid. The wg(8) manual says the interval runs from 1 to 65535 seconds, and that 0 or "off" disables it. Off is the default.

WireGuard is quiet on purpose. The quickstart puts it as "it is not a chatty protocol": a peer transmits only when it has something to send. Between the handshake and the next packet there is nothing on the wire.

That silence is fine when both ends have public addresses. Put one end behind a NAT and it becomes a problem, because the NAT lets a packet in only while it remembers an outgoing one. RFC 4787 defines the mapping timer as "the time a mapping will stay active without packets traversing the NAT" and notes "great variation in the values used by different NATs". Keepalive packets are how an idle peer keeps its mapping inside whatever that timer is.

The setting lives in the peer's own section, on the side that has the NAT:

[Peer]
PublicKey = <the other end's public key>
Endpoint = 203.0.113.2:51820
AllowedIPs = 10.99.0.2/32
PersistentKeepalive = 25

Do I need PersistentKeepalive on both peers?

No. You need it on the peer that is behind NAT and has to be reachable while idle, and nowhere else. The quickstart words the test as a peer behind NAT or a firewall that wants "to be able to receive incoming packets even when it is not sending any packets".

Your setupSet it?Why
Laptop or phone behind home NAT, server with a public address, and only the client opens connectionsOptionalThe client's own traffic keeps the mapping while it is in use
Same client, but the server must reach it while idle (push, ping, remote access)Yes, on the clientNothing else renews the client's mapping
Both peers have public addressesNoNo NAT mapping to keep
Server behind a port forward on a routerUsually no, if the forward is permanentThe router forwards inbound by rule, not by remembered flow
Both peers behind NATEach side needs its own outbound trafficEach NAT keeps its own mapping

The table hides two things. First, the option belongs on the line for the peer you are sending to: the client's [Peer] block for the server holds PersistentKeepalive, and the server's block for the client needs none. Second, a port forward is a rule on the router, not a mapping from a past packet. If your forward is behind carrier-grade NAT (CGNAT) it will never fire, which is a different failure; see port forwarding behind CGNAT.

What happens to a tunnel behind NAT without it?

Without keepalive, the tunnel stops receiving once the mapping expires. In our lab run on 2026-10-29, three pings from the public peer to the idle peer behind the NAT all failed after 70 seconds of quiet. With a keepalive of 25, the same three pings arrived.

How this was tested

Product
WireGuard kernel module with the stock wireguard-tools package (open source; the same upstream tools Debian wireguard-tools ships), three network namespaces
Version
wireguard-tools 1.0.20250521-1ubuntu1, conntrack 1.4.9, tcpdump 4.99.6, kernel 7.0.0-38-generic
System
Ubuntu 26.04.1 LTS virtual machine in our home lab
Checked
Runs
2 without keepalive, 3 with keepalive 25
Not tested

These are the upstream tools Debian wireguard-tools ships. Peer A (192.168.1.2) sat behind a NAT namespace doing masquerade, with the Linux connection-tracking (conntrack) UDP timeouts set to 30 seconds, and peer B (203.0.113.2) was public.

A had B as its endpoint; B had no endpoint for A. Each run: A pings B, B pings A at once, 70 seconds of idle, then B pings A three times. We read wg show on both peers and conntrack on the NAT at each step and counted UDP packets with tcpdump outside.

Keys were throwaway; no ForestVPN production identity was used.

The 30-second timeout is a lab setting, not a claim about any real NAT. Not tested: battery or radio cost, real NATs, other intervals, phone clients, wg-quick, IPv6, or idle times past 70 seconds.

RunPersistentKeepaliveB to A after idleNAT entry for A's flow after idleUDP packets on NAT outside
1off0 of 3none13
2off0 of 3none13
3253 of 3present, assured, 19 s left15
4253 of 3present, assured, 19 s left15
5253 of 3present, assured, 19 s left15

What the NAT held after 70 seconds without keepalive

In runs 1 and 2 the NAT held no entry for A's flow after the idle period. B's pings hit the NAT's own address, found no mapping, and went nowhere. wg show on B still listed A's endpoint and a handshake from about a minute earlier, so it looked healthy while nothing arrived.

What changes with a keepalive of 25

With keepalive 25, the capture on the NAT's outside shows 32-byte packets from A at about 10, 35 and 60 seconds after the handshake: the first fires about 10 seconds in, then one every 25 seconds. Each one restarts the 30-second timer, so the entry was still present when B's pings came about 70 seconds after the idle period began.

One timeline, same 30-second NAT timer in the lab. The only change is the keepalive.

A common mix-up: the keepalive did not refresh the handshake. latest handshake kept ageing in all three keepalive runs. It renews the NAT mapping and nothing else.

One quirk: in all three keepalive runs A's first ping failed and the handshake finished a few seconds later. We did not chase it; the idle and post-idle steps are the comparison.

Why is PersistentKeepalive 25 seconds?

Because the WireGuard documentation calls 25 a sensible interval, and says nothing more than that. The quickstart words it as "a sensible interval that works with a wide variety of firewalls is 25 seconds". The wg(8) manual says only that an idle peer behind NAT "might benefit" from 25, and that most users will not need it. Neither page derives the number.

What the number must beat is the NAT's mapping timer, and that varies. RFC 4787 requires, in REQ-5, that a UDP mapping timer "MUST NOT expire in less than two minutes" and recommends five minutes or more, with an exception for well-known ports below 1024. Real devices do not all follow it, and a stateful firewall on a path can be stricter than the NAT.

A value well under two minutes covers a NAT that follows the rule and many that are stricter. When unsure, err toward the smaller interval: it costs a few extra packets, while a larger one can miss the timer.

Three practical points follow:

  • 25 is a starting value, not a law. If inbound still dies, try a smaller one, such as 15, which a community guide, VPNsmith, also suggests as a next step.
  • Any value safely below the mapping timer is enough. A shorter interval only adds packets.
  • In our run we chose the NAT timer ourselves and set the interval just under it. That shows the mechanism, not that 25 is right for your network.

How do I set PersistentKeepalive and check it works?

Add one line to the peer section, apply it, and read it back from the running interface. These steps are for the peer behind the NAT.

  1. Open the config for the interface and find the [Peer] block for the other end.
  2. Add PersistentKeepalive = 25. With wg-quick, save the file and run wg-quick down then wg-quick up for that interface. To change a live interface without a restart, run wg set <interface> peer <public-key> persistent-keepalive 25.
  3. Run wg show <interface>. The peer should list persistent keepalive: every 25 seconds.
  4. Idle the tunnel for longer than your suspected NAT timer, then send something inbound from the other peer, such as a ping to the tunnel address.
  5. If it still fails, capture UDP on the NAT's outside interface with tcpdump and look for a small packet from your peer at the interval you set.

No such line in wg show? Then the setting did not apply, usually because it sits in the wrong [Peer] block or the file was not reloaded. Mind what wg show cannot tell you. It reports the handshake and counters, and neither proves the NAT still remembers the flow. The inbound test in step 4 is the real check.

If you also run into a handshake that never completes, that is a separate problem; start from The server is not answering connection attempts or the general VPN not connecting guide.

Does PersistentKeepalive drain a phone battery?

The packet is not the cost. Waking the radio is. A keepalive is an empty authenticated packet, which the wg(8) manual calls an "authenticated empty packet", so the data it adds is tiny. On a phone, each send is also a reason for the modem or Wi-Fi chip to leave its low-power state. That wake-up, repeated on a timer while the screen is off, is the part that costs charge. VPNsmith makes the same point.

We did not measure battery or radio use, so we give no figure. The mechanism points to a rule of thumb:

  • If the phone only starts connections, such as browsing or apps, its own traffic renews the mapping and it can leave the option off.
  • If something must reach the phone while it idles, a keepalive is the price of being reachable.
  • A larger interval is cheaper but only works if it is still under your NAT's timer.

Can a keepalive fix a tunnel that still will not receive?

Only if an expired mapping was the problem. If the peer has a keepalive and inbound still fails, check these in order:

  1. The option sits on the peer that is behind NAT, in the block for the endpoint it connects to.
  2. The interval is shorter than the NAT's timer. Lower it and retest.
  3. The peer you are trying to reach actually has an endpoint for the NAT'd peer. In our run, B only learned A's address after A sent first; roaming means the endpoint follows the last authenticated packet the other side saw.
  4. The NAT is CGNAT. A keepalive keeps a CGNAT mapping like any other, within its timer; what cannot work is an inbound port forward, however often you send. See CGNAT.

ForestVPN's apps write the tunnel configuration for you, keepalive included, so there is no file to edit. For running the protocol yourself, see the measured WireGuard versus OpenVPN comparison and the OpenWrt WireGuard setup guide. The WireGuard comparison sets running the protocol yourself against using a service.

Questions and answers

What does PersistentKeepalive do in WireGuard?

It makes a peer send an authenticated empty packet to the other end at a fixed interval in seconds. A NAT or stateful firewall on the path sees outgoing traffic and keeps its mapping, so packets from the other peer can still be delivered. The default is off.

Do I need PersistentKeepalive on both peers?

No. Set it on the peer that sits behind NAT and must be reachable while idle, on the line for the peer it connects to. A peer with a public address and no NAT in front of it needs none. If both peers are behind NAT, each side needs its own outbound traffic to keep its own mapping.

Why is PersistentKeepalive 25 seconds?

The WireGuard quickstart calls 25 seconds a sensible interval that works with a wide variety of firewalls; wg(8) says only that an idle peer behind NAT might benefit from it. It is a margin below short mapping timers, not a derived constant. If your NAT drops mappings faster, use a smaller number.

Does PersistentKeepalive drain a phone battery?

The packet is tiny, so data is not the cost. Each packet is a reason for the radio to leave its low-power state, and that wake-up is where a phone pays. If a phone only sends traffic and never waits for inbound, leave the option off.

How do I tell the keepalive is working?

Run wg show on the peer and look for a persistent keepalive line under the peer. To prove the mapping is renewed, capture UDP on the NAT's outside interface and look for a small packet at the interval you set, then try an inbound packet after the idle time.

Why does the tunnel stop receiving after a few minutes idle?

A NAT or stateful firewall forgets an idle UDP flow when its mapping timer runs out. Packets from the other peer then reach the NAT's address with no mapping to follow. Outgoing traffic from the peer behind the NAT recreates the mapping, and a keepalive does that on a timer.

Sources

  1. WireGuard Quick Start (read October 29, 2026)
  2. wg(8) manual page (read October 29, 2026)
  3. RFC 4787: Network Address Translation (NAT) Behavioral Requirements for Unicast UDP (read October 29, 2026)
  4. ArchWiki: WireGuard (read October 29, 2026)
  5. VPNsmith: WireGuard PersistentKeepalive (read October 29, 2026)

How this was made

Drafted with AI assistance from the WireGuard quickstart, the wg(8) manual, RFC 4787 and two community pages read on 2026-10-29, plus a lab run of two WireGuard peers and a NAT in network namespaces, checked by automated gates and an editor.

All posts

Back to top

Ask us anything