Guides, Nov 5, 2026

# WireGuard: Handshake did not complete after 5 seconds, retrying

The line means your side sent a handshake initiation and did not get a valid answer within 5 seconds, so WireGuard is sending another one. It never says why. Usually the packet did not reach the server (wrong endpoint, wrong port, UDP blocked), or the server dropped it without a reply because the keys do not match its peer list. One tcpdump on the server tells you which of the two it is.

Tahir Mehmood, 8 min read

WireGuard is quiet by design. A server that receives a packet it can't authenticate drops it and sends nothing back, so the client has one piece of information: no answer came. Everything below is about finding out which side the answer went missing on. I work through it in this order, and most of the time I stop at the second step.

## Where does the "Handshake did not complete" line come from?

It comes from the retransmit timer. Both the Linux kernel module and wireguard-go print it, in slightly different formats.

The Linux kernel module only prints it when dynamic debug is on. It is off by default, which is why many people never see it at all:

```
# echo module wireguard +p > /sys/kernel/debug/dynamic_debug/control
# dmesg -wT | grep wg0
wg0: Handshake for peer 5 (176.61.144.20:51820) did not complete after 5 seconds, retrying (try 2)
```

Turn it off again with `-p` instead of `+p` when you are done. The number after "peer" is an internal counter, not a key. The address in parentheses is the endpoint the initiation was sent to, and it is worth reading carefully: it is the first place a wrong IP or port shows up.

wireguard-go, which the official iOS and macOS apps use (and the Android app, unless it finds the kernel module), prints the same event with a shortened public key:

```
peer(Xy4b…Qk8c) - Handshake did not complete after 5 seconds, retrying (try 2)
```

The key is shortened to its first four and last four characters (before the trailing `=`). On Android the line is in the app under Settings, View application log. If you run wireguard-go yourself, start it with `LOG_LEVEL=verbose`, as the [WireGuard quick start](https://www.wireguard.com/quickstart/) describes.

## What do the 5-second and 120-second timers mean?

The 5 seconds is `REKEY_TIMEOUT`, a fixed constant in the protocol. WireGuard sends a handshake initiation, waits 5 seconds plus a random jitter of up to 333 ms, and sends a new one if no valid response arrived. These are the timers that matter here, as defined in the [protocol description](https://www.wireguard.com/protocol/) and the kernel's [timers.c](https://github.com/torvalds/linux/blob/master/drivers/net/wireguard/timers.c):

| Timer | Value | What it does |
| --- | --- | --- |
| `REKEY_TIMEOUT` | 5 s + up to 333 ms | Time to wait for a response before resending the initiation |
| `REKEY_ATTEMPT_TIME` | 90 s | How long to keep retrying for packets that are waiting to be sent |
| `KEEPALIVE_TIMEOUT` | 10 s | Send an empty keepalive if we received data but sent nothing back |
| `REKEY_AFTER_TIME` | 120 s | The initiator starts a new handshake when its session is this old |
| `REJECT_AFTER_TIME` | 180 s | A session key older than this is never used again |

When nothing answers, the log counts up from "try 2" to "try 20", one line about every 5 seconds, and then gives up:

```
wg0: Handshake for peer 5 (176.61.144.20:51820) did not complete after 20 attempts, giving up
```

At that point WireGuard drops the packets it was holding. The counter resets as soon as something new wants to go through the tunnel, so on a busy client you may never see "giving up". You see the tries climb, stop, and start again from 2.

A different line, `Retrying handshake ... because we stopped hearing back after 15 seconds`, means something else: a session existed, data was sent, and nothing came back for `KEEPALIVE_TIMEOUT + REKEY_TIMEOUT`. That is a tunnel that was working and broke, often because the server restarted or the client's network changed.

### How often does WireGuard do a handshake?

Every 2 minutes while traffic flows, and never while the tunnel is idle. The initiator starts a new handshake when it sends data on a session older than `REKEY_AFTER_TIME`. If no one sends anything, no handshake happens, so `latest handshake: 3 hours, 12 minutes, 5 seconds ago` on an idle peer is normal, not a fault. Peers with `PersistentKeepalive` send a packet at every interval (25 seconds is typical), so their handshake age should stay under about two and a half minutes.

## Step 1: Is the initiation reaching the server?

Run tcpdump on the server while the client tries to connect. This one command splits the problem in half:

```
# tcpdump -ni any udp port 51820
IP 176.61.144.77.41237 > 176.61.144.20.51820: UDP, length 148
IP 176.61.144.20.51820 > 176.61.144.77.41237: UDP, length 92
```

WireGuard message sizes are fixed, so the length tells you the type: 148 is a handshake initiation, 92 is a handshake response, 64 is a cookie reply, 32 is a keepalive, and anything else is data. You will see one of three patterns:

1.  **Nothing arrives.** The problem is between the client and the server: the endpoint, the port or the network. Go to causes 1 and 2.
2.  **148 arrives, nothing goes back.** The server received the initiation and rejected it. Go to causes 3, 4 and 5.
3.  **148 arrives and 92 goes back, but the client keeps retrying.** The server is happy and the client rejects the answer, or never receives it. Go to cause 6.

On the client side, `wg show` gives the same picture from the other end:

```
peer: Hk3vR8bPq2LmNw5sYt7uZx9aCd1eFg3hJk5lMn7oPqE=
  endpoint: 176.61.144.20:51820
  allowed ips: 0.0.0.0/0
  transfer: 0 B received, 1.16 KiB sent
```

There is no `latest handshake` line, because none ever completed. The sent counter grows by 148 bytes per try (8 tries is 1.16 KiB), and received stays at zero.

## 1\. Is the endpoint address and port right?

Check this first, because it is the cheapest to check and the most common. The `Endpoint` in the client's `[Peer]` section must match the server's public address and its `ListenPort`. On the server:

```
# wg show wg0 listen-port
51820
```

If the server config has no `ListenPort` line, WireGuard picks a random port every time the interface comes up. The tunnel works until the first reboot and then never again. Port 51820 is only a convention from the quick start; nothing enforces it.

If the endpoint is a hostname, `wg` resolves it once, when the config is applied. If the server's IP changes later, the client keeps sending to the old address. `wg show wg0 endpoints` on the client shows the address actually in use. wireguard-tools ships a [reresolve-dns](https://git.zx2c4.com/wireguard-tools/about/contrib/reresolve-dns/README) script for servers on dynamic DNS.

## 2\. Is UDP reaching the server at all?

If tcpdump shows nothing, test the UDP path without WireGuard in the way. Keep tcpdump running on the server and send any UDP packet from the client:

```
$ echo test | nc -u -w1 176.61.144.20 51820
```

A 5-byte UDP packet should appear in tcpdump. WireGuard will ignore it, which is fine: you are testing the path, not the server. As the [port monitoring post](https://tunnelhq.com/blog/why-wireguard-port-monitoring-fails/) explains, the server never answers a packet like this, so only the tcpdump on the server side tells you anything.

If the 5 bytes don't arrive, look at:

-   The server firewall: `nft list ruleset`, `iptables -S INPUT` or `ufw status`. The WireGuard port must be open for UDP, not only TCP.
-   The cloud provider's firewall or security group. It sits outside the VM, so nothing on the server shows it.
-   The client's network. Hotel, office and some mobile networks block or filter UDP on unknown ports. Try the same config from a phone hotspot. If it works there, the server is fine.

## 3\. Does the server know this client's public key?

If the initiation arrives and nothing goes back, the most common reason is that the client's public key is not in the server's peer list. The server can decrypt enough of the initiation to see who claims to be calling, finds no matching peer, and drops it. With kernel debug on, the server logs:

```
wg0: Invalid handshake initiation from 176.61.144.77:41237
```

wireguard-go logs `Received invalid initiation message from 176.61.144.77:41237` instead. Compare the keys directly. On the client, derive the public key from the private key in its config, then look for it on the server:

```
$ echo 'cOm9ZqH4vB2nX8sK1tL6wR3yE5uI7oP0aS4dF6gJ8hM=' | wg pubkey
# wg show wg0 peers
```

A frequent version of this: the peer was added with `wg set` on a running server and never written to the config file, so it disappeared at the next restart. Either add peers to the file and apply them with `wg syncconf wg0 <(wg-quick strip wg0)`, or use `SaveConfig = true` in the server's `[Interface]`.

## 4\. Does the client have the server's real public key?

A wrong server key in the client config fails one step earlier. The first MAC in every initiation is keyed with a hash of the server's public key, so the server rejects the packet before it even looks at the peer list:

```
wg0: Invalid MAC of handshake, dropping packet from 176.61.144.77:41237
```

wireguard-go logs `Received packet with invalid mac1`. Compare `wg show wg0 public-key` on the server with the `PublicKey` in the client's `[Peer]` section. This one usually appears after a server is rebuilt and its keys are generated again, while old client configs stay in circulation.

## 5\. Did the client's clock go backwards?

This is the cause people find last. Every initiation carries an encrypted TAI64N timestamp from the client's clock. To stop replays, the server remembers the newest timestamp it has accepted from each peer and rejects any initiation that is not newer.

So if a client's clock is now earlier than it was at its last successful handshake, the server rejects every initiation until the clock passes that point. The typical case is a router or small board without a real-time clock: it boots with a date in the past and gets nothing until NTP corrects it. The other case is a device whose clock was once set in the future, connected, and was then corrected. The server's log line is the same `Invalid handshake initiation` as for an unknown key, which is what makes this one confusing. If the key is right and you still see that line, check `date` on the client.

A clock that is simply behind is fixed by NTP on the client. For the future-clock case, a correct clock is rejected until that future time arrives, so clear the state on the server instead. It is kept in memory only, so removing the peer and adding it back, or restarting the server's interface, resets it:

```
# wg set wg0 peer <client-public-key> remove
# wg syncconf wg0 <(wg-quick strip wg0)
```

## 6\. Why does the server answer but the client still retry?

If tcpdump shows 148 in and 92 out, the server is doing its job. Two things are left.

**A preshared key mismatch.** The `PresharedKey` is mixed into the key derivation only in the response, not in the initiation. So the server accepts the initiation, logs `Receiving handshake initiation` and `Sending handshake response`, and looks completely healthy. The client cannot decrypt the response and logs:

```
wg0: Invalid handshake response from 176.61.144.20:51820
```

(wireguard-go: `Received invalid response message from 176.61.144.20:51820`.) Compare the output of `wg show wg0 preshared-keys` on both sides. Either both have the same key for this peer, or neither has one.

**The response doesn't get back.** Run tcpdump on the client too. If no 92-byte packets arrive there, something between the two drops them: a stateful firewall or NAT on the client's side, or a server with several addresses answering from a different source IP than the one the client sent to.

## Why it is usually not AllowedIPs or MTU

Both come up often in threads about this error, and both are rarely the cause.

**AllowedIPs is not checked during the handshake.** On the client it decides which packets are sent to the peer, so it decides whether a handshake is started at all. On the server it decides which inner source addresses are accepted after the handshake. A wrong value gives you a completed handshake and no traffic, with debug lines like `Packet has unallowed src IP (10.0.0.9) from peer 5` or `No peer has allowed IPs matching 10.0.0.1`. It does not give you "did not complete".

**MTU rarely touches the handshake.** The initiation is 148 bytes and the response is 92. They fit through any real network path. MTU problems show up as a tunnel that connects, answers small pings, and then hangs on large transfers.

Two rarer causes are worth a sentence each. If the same client key is used on two devices at once, each new handshake from one device replaces the other's session, and both tunnels keep breaking. A server under a flood of initiations answers with 64-byte cookie replies instead of responses (debug line: `Sending cookie response for denied handshake message`), which standard WireGuard clients handle.

## How do I force a WireGuard handshake?

There is no command for it. A handshake starts when there is something to send, so send something:

```
$ ping -c 1 10.0.0.1
```

Use an address inside the peer's `AllowedIPs`, such as the server's tunnel address. Another way on Linux is to turn on persistent keepalive for the peer: when it changes from off to on on a running interface, the kernel sends a keepalive immediately, which starts a handshake if there is no session.

```
# wg set wg0 peer <server-public-key> persistent-keepalive 25
```

A server can only start a handshake towards a client whose endpoint it knows. A roaming client that has never connected has no endpoint on the server, and the kernel logs `No valid endpoint has been configured or discovered for peer 5` if you try to send to it.

## How do I catch this before users report it?

On the server, the cheapest check is the handshake age of peers that should always be up, such as site-to-site links or anything with `PersistentKeepalive`:

```
#!/bin/sh
# Print peers on wg0 with no handshake in the last 180 seconds
now=$(date +%s)
wg show wg0 latest-handshakes | while read -r key ts; do
  if [ "$ts" -eq 0 ]; then echo "$key never"; continue; fi
  age=$((now - ts))
  [ "$age" -gt 180 ] && echo "$key ${age}s"
done
```

This has two blind spots. It only works for peers that send traffic all the time, because idle peers have old handshakes by design. And it runs on the server, so it sees nothing when the server itself is unreachable, which is exactly when you need to know. The [VPN server monitoring guide](https://tunnelhq.com/blog/vpn-server-monitoring-guide/) covers what else to check and from where.

For checks from outside, full disclosure: [TunnelHQ](https://tunnelhq.com/wireguard-monitoring/) completes a real WireGuard handshake against your server from its own checkers every 1 to 5 minutes (depending on plan), using a config you paste in, and alerts when it fails. A failed handshake shows as "Tunnel unreachable". To check one config once, paste it into the [free tester](https://tunnelhq.com/test/), no account needed.

Source: https://tunnelhq.com/blog/wireguard-handshake-did-not-complete/
