Monitoring

VPN monitoringUptime monitoringIntegrations

Platform

AI assistantsAPI and CLISecurity

More

PricingFree testerDocsBlog Log in

Guides

Xray REALITY not working: how to find the real cause

When REALITY rejects a client, the server sends no error. It forwards the connection to the target website, so the client only sees that site's real certificate and logs "received real certificate" or "reality verification failed". To find the cause, set the server's log level to info and read the reason after "REALITY: processed invalid connection". In most cases it is a key pair that doesn't match, a shortId or SNI that isn't on the server's list, or a target site without TLS 1.3. Since Xray-core v26.9.9 there is one more: the server rejects clients whose Client Hello has no X25519MLKEM768 key share, which today includes sing-box.

ST Muntaha10 min read

If you are in a hurry, this is the order I check things in: the server's reject reason, the key pair, the shortId, the SNI against serverNames, the target site, and then the client's core and fingerprint. Everything below is checked against Xray-core v26.10.31, the REALITY library it ships with, and sing-box 1.14.4, the current releases on 1 November 2026.

Why does REALITY fail without a clear error?

Because hiding is its job. A REALITY server that receives a TLS connection it cannot authenticate does not reset it or send an alert. It connects to the target you configured and relays the bytes both ways, so whoever is probing gets the target website's genuine TLS handshake and certificate. That is what makes a REALITY server look like an ordinary website to a scanner.

For you, it means a broken client and a probe look the same from outside. The client completes a TLS handshake, but with the real certificate instead of REALITY's temporary one, and gives up. Xray clients log:

REALITY: received real certificate (potential MITM or redirection)
REALITY: processed invalid connection

sing-box clients log reality verification failed. Neither message says why. The reason is only on the server.

How do I see why the server rejected a connection?

Raise the server's log level to info. The REALITY reject message is logged at that level, so the default warning level hides it:

"log": { "loglevel": "info" }

Restart Xray, follow the log (journalctl -u xray -f on an install made with the official script), and connect once from the failing client. Look for a line like this, with your client's IP:

REALITY: processed invalid connection from 176.61.144.77:51834: authentication failed or validation criteria not met

The part after the last colon is the reason. Your server also logs these lines for scanners and other probes from the internet. That is normal; only the lines from your client's address matter.

What do the "processed invalid connection" reasons mean?

There are six reasons in the REALITY source, and each one points to a different place:

Reason What happened Check
failed to read client hello The first bytes were not a TLS Client Hello Client security, a proxy in front, PROXY protocol
unsupported TLS version: 303 The client offered TLS 1.2 at most (303 is hex 0x0303) Client fingerprint, old client
server name mismatch: <sni> The SNI is not in serverNames Link sni against the server list
authentication failed or validation criteria not met Keys, shortId, client version limits, time limit, or no X25519MLKEM768 key share Sections below
target sent incorrect server hello or handshake incomplete The target did not answer with a usable TLS 1.3 Server Hello The target site
handshake did not complete successfully The REALITY handshake started but did not finish The client gave up, or the network between client and server

A separate message, REALITY: failed to dial dest: ..., means the server could not even connect to its target: a DNS failure on the server, an egress firewall, or a target that resolves to IPv6 when the server has no IPv6 route.

The most common reason by far is the fourth one, and it covers several causes. To tell them apart, turn on REALITY's debug output for a few minutes with "show": true in the server's realitySettings. Xray then prints the steps of each handshake to stdout, which ends up in the same journal:

REALITY remoteAddr: 176.61.144.77:51834	hs.c.AuthKey[:16]: [...]	AEAD: ...
REALITY remoteAddr: 176.61.144.77:51834	hs.c.ClientVer: [26 10 31]
REALITY remoteAddr: 176.61.144.77:51834	hs.c.ClientTime: 2026-11-10 09:14:02 +0000 UTC
REALITY remoteAddr: 176.61.144.77:51834	hs.c.ClientShortId: [72 4 127 173 42 2 205 36]
REALITY remoteAddr: 176.61.144.77:51834	hs.c.conn == conn: false

Read it like this:

  • No hs.c.conn == conn line at all for your client, and the reason says authentication failed: the server stopped before it tried the keys. With the SNI and TLS version already fine, that means the Client Hello had no X25519MLKEM768 key share. See the sing-box section below.
  • An AuthKey line, but no ClientVer, ClientTime or ClientShortId lines: the server could not decrypt the client's auth block. The client's public key (password, or pbk in the link) does not match the server's private key.
  • All the lines, ending in hs.c.conn == conn: false: the keys are fine, but a check failed. Compare the printed shortId with your shortIds list (it prints in decimal bytes: [72 4 127 173 42 2 205 36] is 48047fad2a02cd24), and the version and time with minClientVer, maxClientVer and maxTimeDiff if you set them.
  • hs.c.conn == conn: true, but the client still fails: authentication worked, so the problem is the target or the network after it.

Turn show off again when you are done. It prints a lot.

Why do sing-box clients fail with "reality verification failed" on new servers?

Because the REALITY library in Xray-core v26.9.9 and later rejects any Client Hello that does not carry an X25519MLKEM768 key share before its X25519 share. The commit message calls these Client Hellos "outdated/strange". Current browsers send that post-quantum key share, and so does Xray's chrome fingerprint.

sing-box's REALITY client removes X25519MLKEM768 from the Client Hello before sending it. I checked this in common/tls/reality_client.go in sing-box 1.14.4 and in the 1.15.0 alpha. So against a server on Xray-core v26.9.9 or later, every sing-box REALITY connection fails. The client logs reality verification failed, and the server logs authentication failed or validation criteria not met. The keys can be perfect. This also hits apps that run sing-box as their core, and it appears on 3x-ui servers as soon as the panel's Xray binary is v26.9.9 or later. The problem is tracked in sing-box issue #4520, which was still open on 1 November 2026.

How to confirm it is this problem and not your keys: the same link works in an Xray-based client (v2rayNG, or v2rayN set to use Xray-core), fails in a sing-box-based one, and show: true prints no hs.c.conn == conn line for the sing-box client.

What you can do until sing-box ships a fix:

  • Move affected users to an Xray-based client.
  • Give sing-box users a second inbound that does not use REALITY, for example VLESS over TLS with your own domain and certificate.
  • Run the server on an Xray-core version older than v26.9.9. I don't recommend it: v26.10.21 and v26.10.31 are security releases (the first removed three root certificates after the private keys of intermediates under them were cracked, the second turned off MPTCP by default after a Linux kernel vulnerability), and you would give those up.

The same check affects Xray clients with some fingerprints. In the uTLS version that Xray-core v26.10.31 uses, chrome, firefox and safari send X25519MLKEM768. ios, edge and qq offer TLS 1.3 without it, so new servers reject them. android and 360 do not offer TLS 1.3 at all, so REALITY cannot work with them. If a link has fp=ios, fp=edge, fp=qq, fp=android or fp=360, change it to chrome.

How do I check that the client's public key matches the server's private key?

Derive the public key from the private key on the server and compare it with the pbk in the link:

xray x25519 -i "WIjetb6W4iVMPUNHFgXWPNIwcHmyAZmYKnjpnN5nCl0"
PrivateKey: WIjetb6W4iVMPUNHFgXWPNIwcHmyAZmYKnjpnN5nCl0
Password (PublicKey): DVm-D2KuvBWxtDYfTW6rxE-9YY7q53V9HQ413NryNw0
Hash32: ...

The Password (PublicKey) line must equal the client's password (publicKey in older configs, pbk in links), character for character. Xray keys are unpadded base64url, so - and _ are part of the key. Copying a key through a chat app or a spreadsheet that "fixes" characters is a classic way to break it. Hash32 is not used by REALITY. (These keys are examples. Generate your own with xray x25519.)

If they don't match, someone generated a new key pair on the server and the links were never updated. Check whether the inbound was edited recently, in the panel or in the config file.

What is wrong with my shortId?

The shortId is 0 to 16 hex characters (8 bytes), and the client's value must be one of the server's shortIds. The rules from the REALITY docs:

  • Hex digits only (0 to 9, a to f), and an even number of characters. aa1234 is valid; aaa1234 fails with invalid "shortId" on the client.
  • Shorter values are padded with zeros, so aa1234 and aa12340000000000 are the same ID.
  • An empty client shortId works only if the server's list contains "".
  • The server needs at least one entry; an empty list fails with empty "shortIds".

How do I check the target site?

The target (target, formerly dest) must be a site that REALITY can imitate. The REALITY README sets the minimum: it supports TLS 1.3 and HTTP/2, the domain is not used only for redirects, and it sits outside the censoring country. Check it from the server, because the server is what connects to it:

xray tls ping www.example.com
curl -s -o /dev/null -w '%{http_version} %{http_code}\n' https://www.example.com

xray tls ping should show TLS Version: TLS 1.3 in the "Pinging with SNI" block. It also prints TLS Post-Quantum key exchange and the certificate's allowed domains, which tell you which names you can put in serverNames. The curl line should print 2 200. A 301 or 302 means the domain redirects; use the domain it redirects to.

Then check the target through your REALITY server, the way an unauthenticated visitor would see it:

xray tls ping -ip 176.61.144.20 www.example.com

If this fails while the direct ping works, the server cannot reach the target. Look for failed to dial dest in its log.

Two warnings that Xray-core v26 prints at startup are worth reading, not ignoring. It warns when a server name contains apple, icloud or microsoft or ends in .ru, .ir or .cn, and when a REALITY inbound listens on a port other than 443. Both say the setting makes it more likely that the server's IP gets blocked.

Does the clock matter for REALITY?

Only if you set maxTimeDiff. The client puts its current time into the auth block, but the server checks it only when maxTimeDiff is above 0, and the value is in milliseconds. With "maxTimeDiff": 60000, a client whose clock is more than a minute off fails with authentication failed or validation criteria not met, and show: true prints its ClientTime, so you can see how far off it is. The same applies to minClientVer and maxClientVer: a client outside the range fails the same way, and ClientVer shows its version.

What if something else is on port 443?

Xray can only bind the port if nothing else holds it. If nginx or another service already listens on 443, Xray fails at startup with failed to listen TCP on 443 and address already in use. ss -ltnp 'sport = :443' shows which process has it.

A common way to share 443 between a website and REALITY is nginx's stream module with ssl_preread, which routes by SNI to Xray on a local port. Two things break here:

  • If nginx sends the PROXY protocol (proxy_protocol on;) and Xray does not expect it, REALITY reads PROXY TCP4 ... instead of a Client Hello, and every connection fails with failed to read client hello. Set "rawSettings": {"acceptProxyProtocol": true} in the inbound's streamSettings, or turn it off in nginx. It has to match on both sides.
  • Xray's "non-443 port" warning appears, because the inbound listens on the local port. In this setup it is harmless: the port the internet sees is still 443.

What if REALITY works from one network but not another?

Then the server is fine, and something on the path is interfering: the server's IP may be blocked, the SNI of your target may be filtered, or TCP to port 443 may be throttled on that network. From the server's side there is often nothing to see, because the connections never arrive. To tell a block from an outage, test from outside the affected network and from inside it, through a user, a volunteer, or a measurement platform such as OONI Probe or RIPE Atlas. If only some networks fail, changing the target domain or the server's IP is a reasonable next step. Changing keys will not help.

Full disclosure: catching these failures early is what TunnelHQ is for. It completes the real REALITY handshake from the vless:// link, authenticates and sends requests through the tunnel every 1 to 5 minutes depending on plan, and opens an incident when the check fails, so a key pair change or a target that stopped answering shows up as an incident, not as a user complaint. On higher plans you can pick the countries a tunnel is tested from. VLESS monitoring explains the check. If a key fails and you are not sure the server is the problem, test the link layer by layer as in how to check a VLESS key, and see VLESS protocol explained for how REALITY fits around VLESS.