VLESS key checker: how to test a vless:// link properly
To check a VLESS key, do what a real client does: read the vless:// link, resolve the host, open the TCP connection, finish the TLS or REALITY handshake, pass VLESS authentication with the UUID, and send a real request through the tunnel. Only that last step proves the key works. The honest quick test is to load the link into Xray-core on a machine you control and fetch a URL through it with curl, or to paste the link into a tester that runs a real client for you.
ST Muntaha11 min read
People call a vless:// link a "key", and they want one answer: does it work? The trouble is that a link can fail at six different layers, and most quick checks only test the first one or two. This post goes through the layers, how to test each one from the command line, and what the failure looks like at each step. I checked the commands and error strings against Xray-core v26.10.31, the current release on 1 November 2026.
What does checking a VLESS key actually test?
A full check passes six layers, and a key is only good if all six pass:
- The link parses. UUID, host, port,
type,securityand the TLS or REALITY fields are present and valid. - The host resolves to an IP address that is really your server.
- The port answers. A TCP connection opens (UDP for mKCP).
- The TLS or REALITY handshake completes with the right SNI, certificate or REALITY key.
- VLESS accepts the UUID, and the
flowmatches what the server expects. - Real traffic flows. A request through the tunnel reaches the internet and the reply comes back.
Layers 1 to 4 can all pass while the key is useless. A server that deleted the user still resolves, still accepts TCP and still completes TLS. It refuses only at layer 5. A REALITY server goes further and completes a TLS handshake for anyone, as I explain below. So a check that stops before layer 6 can tell you a key is broken. It can never tell you a key works.
How do I read the fields of a vless:// link?
A vless:// link is a URL. Its format is the share link standard that the Xray project keeps in discussion #716. Here is a typical REALITY link, split over several lines so it is easier to read (a real link is one line):
vless://[email protected]:443
?type=tcp
&encryption=none
&security=reality
&sni=www.example.com
&fp=chrome
&pbk=DVm-D2KuvBWxtDYfTW6rxE-9YY7q53V9HQ413NryNw0
&sid=48047fad2a02cd24
&spx=%2F
&flow=xtls-rprx-vision
#de-fra-01
The part before @ is the user ID (a UUID). Then come the host and the port. Everything after ? is a parameter, and the text after # is only a display name. Parameter names and fixed values such as reality are case-sensitive, and values are percent-encoded (%2F is /).
| Field | What it holds | Xray config key |
|---|---|---|
type |
Transport: tcp (RAW), kcp, ws, grpc, httpupgrade, xhttp |
streamSettings.network |
encryption |
VLESS Encryption; none unless the server uses it |
settings.encryption |
flow |
xtls-rprx-vision, or empty |
settings.flow |
security |
none, tls or reality |
streamSettings.security |
sni |
Server name in the TLS Client Hello; the host if omitted | serverName |
fp |
uTLS fingerprint; chrome if omitted; required for REALITY |
fingerprint |
alpn |
TLS ALPN values, comma-separated | tlsSettings.alpn |
pbk |
REALITY public key (Xray now calls it the password) | realitySettings.password |
sid |
REALITY short ID in hex; may be empty | realitySettings.shortId |
spx |
REALITY spider path | realitySettings.spiderX |
pqv |
REALITY ML-DSA-65 public key; optional | realitySettings.mldsa65Verify |
path, host |
Path and Host header for ws, httpupgrade and xhttp | wsSettings and the others |
serviceName, mode |
gRPC service name, and gun or multi |
grpcSettings |
mode, extra |
XHTTP mode and its extra settings as JSON | xhttpSettings |
pcs, vcn |
Pinned certificate SHA-256, and the name to verify | pinnedPeerCertSha256, verifyPeerCertByName |
One field you will still see in old links is not in the table: allowInsecure. Xray-core removed that option in v26.2.6. A config that sets it now fails to load with The feature "allowInsecure" has been removed, and the standard says to use pcs and vcn instead. So a link for a server with a self-signed certificate stops working once the user updates the client, until you add the certificate hash as pcs.
Why does my client's ping say the key works when it doesn't?
Because most "ping" buttons only test layer 3. v2rayN has "Test tcping" and "Test real delay". v2rayNG has "Test TCP delays (TCPing)" and "Test real delays". TCPing measures how long it takes to open a TCP connection to the host and port. It never sends a VLESS header, so it passes for a deleted user, a wrong UUID, a wrong REALITY key or the wrong transport. A good TCPing only means the port is open.
The real delay test is much better. The client starts the proxy and fetches a URL through it (the URL is in the client's settings). If it shows a time, the key worked at that moment, from your network, with your client version. That last part matters. A pass on your home connection does not show that the key works for users on a mobile network in another country, and a failure may be your own network, not the server.
REALITY adds a trap of its own. When a REALITY server gets a TLS connection that does not authenticate, it does not refuse it. It forwards the connection to the real website set as its target, so the visitor receives that website's genuine certificate. This is how REALITY resists active probing; the REALITY README describes the behaviour. The side effect for testing is simple: openssl s_client against a REALITY server always "succeeds" with a valid certificate, whether your key is right or completely wrong. A TLS check proves nothing about a REALITY key.
How do I check a VLESS key from the command line with Xray-core?
Run a real Xray client with the values from the link, then send a request through it. You need the Xray-core binary and curl, on any machine that is not the server itself.
curl -LO https://github.com/XTLS/Xray-core/releases/download/v26.10.31/Xray-linux-64.zip
unzip Xray-linux-64.zip -d xray-check
cd xray-check
./xray version
Write the link's fields into client.json. This is the link from above, with a local SOCKS port for curl to use:
{
"log": { "loglevel": "info" },
"inbounds": [
{
"listen": "127.0.0.1",
"port": 10808,
"protocol": "socks",
"settings": { "udp": true }
}
],
"outbounds": [
{
"protocol": "vless",
"settings": {
"address": "vpn.yourdomain.com",
"port": 443,
"id": "aafd62da-d5aa-406c-985d-17cc48f142fd",
"encryption": "none",
"flow": "xtls-rprx-vision"
},
"streamSettings": {
"network": "raw",
"security": "reality",
"realitySettings": {
"serverName": "www.example.com",
"fingerprint": "chrome",
"password": "DVm-D2KuvBWxtDYfTW6rxE-9YY7q53V9HQ413NryNw0",
"shortId": "48047fad2a02cd24",
"spiderX": "/"
}
}
}
]
}
Three version notes. The flat settings block (address, port and id at the top level) works from Xray-core v25.9.11; older cores need the vnext form. password was called publicKey before, and both names still work. The current docs call network by a new name, method (from v26.7.11); v26.10.31 accepts both, and I use network so the file also loads on older cores.
Test the config before you run it:
./xray run -test -c client.json
A valid config prints Configuration OK. and exits. Otherwise Xray prints the reason, for example invalid "password" (the pbk is not 32 bytes of base64url) or invalid "shortId" (the sid is not hex, or has an odd number of characters). Those are layer 1 failures: the link itself is broken, and no server change will fix it.
Now start the client and send one request through it:
./xray run -c client.json
# in a second terminal:
curl -sS -o /dev/null -w '%{http_code} %{time_total}s\n' \
-x socks5h://127.0.0.1:10808 https://www.gstatic.com/generate_204
204 and a time means the key works: the request went out through the server and the reply came back. Use socks5h, not socks5. With socks5h the server resolves the domain name, so the test also covers the server's DNS. Keep an eye on the Xray terminal while curl runs; at info level it logs each connection and any error on the way.
How do I check each layer when the full test fails?
When curl fails, test the layers from the bottom up. Each command takes a few seconds.
DNS. Check that the host resolves to your server's address:
dig +short vpn.yourdomain.com
TCP. Check that the port opens from where you are:
nc -vz -w 5 vpn.yourdomain.com 443
A timeout here, while Xray is listening on the server, points to a firewall, a cloud security group or a blocked IP. Panels can do this too: 3x-ui enforces per-client IP limits with Fail2ban and iptables (per its README), so a key can fail from one network and work from another.
TLS, when the link has security=tls. Check the certificate the server presents for the link's SNI, and its dates:
openssl s_client -connect vpn.yourdomain.com:443 -servername vpn.yourdomain.com </dev/null 2>/dev/null \
| openssl x509 -noout -subject -enddate
REALITY, when the link has security=reality. A TLS check passes even with wrong keys, so test what you can: that the server forwards to a working target site. xray tls ping connects to an IP you give it, with the SNI you give it:
./xray tls ping -ip 176.61.144.20 www.example.com
Read the "Pinging with SNI" block. You want Handshake succeeded, TLS Version: TLS 1.3 and the target site's certificate. If this handshake fails, the server cannot reach its target, or the SNI is not a name the target serves. Whether the REALITY key itself is right shows only in the full test and in the server log.
VLESS, layer 5. If the handshake looks fine, the server log has the answer, at info level. On a server installed with the official script, follow it with journalctl -u xray -f and run the test again. The lines that matter:
invalid request user id: <uuid>: the server has no such user. It was removed or disabled, or the link has a typo.account <uuid> is rejected since the client flow is empty: the server's user hasxtls-rprx-vision, but the link has noflow.account <uuid> is not able to use the flow xtls-rprx-vision: the reverse. The link has a flow and the server's user does not.REALITY: processed invalid connection from <ip>: <reason>: REALITY rejected the handshake, and the reason says which check failed.
On the client side, a wrong UUID often shows up as unexpected response version. Expecting 0 but actually 72. That happens when the server has fallbacks: it hands the rejected request to a web server, and the client reads the start of an HTTP reply (72 is the ASCII code of H, the first letter of HTTP/1.1). Without fallbacks the server just closes the connection, and the client logs failed to read response version.
What are the most common reasons a VLESS key fails?
Roughly in the order I see them:
- The user no longer exists on the server. Panels such as 3x-ui have per-client traffic quotas and expiry dates. Once a client hits one, the panel stops that UUID from connecting, and every link with it fails at layer 5.
- The server's keys changed. Someone generated a new REALITY key pair or new short IDs, or replaced the certificate, and the old links were never updated. Every user with an old link fails the same way at the same time, which is the clue.
- The flow does not match. The server's user and the link must agree on
xtls-rprx-visionor no flow. The two error strings above tell you which side is wrong. - The transport details do not match.
type,path,host,serviceNameor the XHTTPmodediffer from the server. If nginx sits in front of Xray, look in its access log for 404 responses on the path. - A newer client refuses an old, insecure link. From v26.7.11, Xray-core will not send VLESS without TLS or REALITY to a public address. The error is
vless without TLS or other encryption is prohibited unless the server address is a private IP or domain. Together with theallowInsecureremoval, this breaks oldsecurity=noneand self-signed links as soon as users update. - The client and the server are on different generations. A link with
encryptionother thannoneuses VLESS Encryption, which sing-box 1.14.4 does not offer, so it works only in Xray-based clients. And REALITY servers on Xray-core v26.9.9 or later reject a Client Hello without the X25519MLKEM768 key share, which sing-box clients do not send at the moment (sing-box issue #4520). - Something on the path between user and server. DNS answers that differ by network, an IP address or SNI that is blocked in the user's country, UDP blocked for mKCP. A test from your own location cannot see any of this. You need a test from the user's network, or at least from more than one place.
How do I keep checking keys after they work?
A key that passes today can fail tomorrow for any reason on that list, and users usually notice before the operator does. Run the full layer 6 test on a schedule from outside the server, and alert when it fails a few times in a row, not on one failure. This small script wraps the test above and exits non-zero when the key does not work, so cron or any scheduler can act on it:
#!/bin/sh
# check-vless.sh: exit 0 if the key works, 1 if it does not
./xray run -c client.json >/dev/null 2>&1 &
pid=$!
sleep 2
code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 10 \
-x socks5h://127.0.0.1:10808 https://www.gstatic.com/generate_204)
kill "$pid"
[ "$code" = "204" ]
For what to check besides the key itself, and how often, see the VPN server monitoring guide. If your keys reach users through a subscription URL, one dead server can hide inside a healthy bundle; monitoring V2Ray subscription URLs covers that case.
Full disclosure: this is the check TunnelHQ runs. You paste the vless:// link or a subscription URL, and it completes the real handshake and sends requests through the tunnel every 1 to 5 minutes depending on plan, with alerts to Slack, email and 18 other channels. VLESS monitoring explains the check, and every plan starts with a 14-day free trial. To check one key once, paste it into the free tester, no account needed.


