How it works
VPN device limits: how HWID binding works and what it cannot stop
A device limit is the rule that one subscription serves one device, or a fixed number of them. It reads like a licensing detail, and commercially that is what it is. The interesting part is where the rule is enforced: not inside the tunnel, and not by the VPN protocol. Once you know which component makes the decision, most of the strange behavior around device limits stops being strange - including why a phone you have owned for two years can suddenly be treated as a new one.
Where the check actually runs
A modern setup gives you a single subscription URL instead of a config file. The client fetches that URL over HTTPS - on import, on a schedule, and whenever you press the refresh button - and gets back a list of servers. That request is ordinary HTTP, and it is the only moment the provider's own application code sees your client at all.
The tunnel is a separate story. It terminates on a node running Xray or sing-box, and that process authenticates a UUID for VLESS or a password for Hysteria2. It has no notion of a device, an account, or a subscription. It checks a credential and forwards packets.
So the device limit lives in the subscription endpoint. On the first fetch the server stores an identifier the client sent alongside the request and ties it to that subscription token. A later fetch carrying a different identifier is refused. Refusal seldom takes the form of an HTTP error, because most clients render an error status as nothing at all - a common workaround is to answer with a valid config list containing a single fake entry whose name is the message, since the server name is the one field every client displays.
What HWID is, and what it is not
The identifier usually travels in a request header such as X-HWID, next to headers describing the OS and client version. HWID is short for hardware ID, which is the first misleading thing about it: the client computes some value it considers stable for this installation and sends it, and nothing on the server verifies where that value came from. What the value is derived from depends entirely on the client.
- On iOS it is typically derived from
identifierForVendor, which is shared by all apps from the same vendor on that device and is regenerated once the last of them is removed. If the VPN client is the only app you have from that vendor, deleting and reinstalling it is enough to change the value. - On Android it is one of two things: an OS-provided ID scoped to the app signing key, the user profile and the device, which survives an uninstall and reinstall and changes on a factory reset; or a random value the client generates once and keeps in its own data directory, which is lost when app data is cleared. Which one you get depends on the client, and that difference explains most of the disagreements about whether reinstalling costs you a slot.
- On desktop it is often a hash of whatever the client can read without special privileges - a machine GUID, a disk identifier, a MAC address - stable until the OS is reinstalled or the hardware changes.
- Clients that never implemented the header send nothing, and the server has nothing to bind.
One more thing about the identifier, in the opposite direction: it is a per-installation ID that your provider now stores next to your subscription. That is unremarkable on its own, but it is a reminder that a subscription is not an anonymous object. A VPN changes the network address a site sees; it does not make you anonymous, and the reasons are covered in the guide on deanonymization through device identifiers.
What a device limit does prevent
- Casual sharing. One link pasted into a group chat stops working for everyone after the first person imports it, which is the entire point.
- Unbounded fan-out. Without a limit, a single paid subscription can quietly become a small free service for a dozen people, and the load lands on the provider's nodes.
- Accidental duplication. The same link imported on a work laptop a year ago and forgotten there is still a device consuming a slot, and the limit makes that visible.
- Predictable pricing. Charging per device instead of per byte only works if the number of devices is countable at all.
None of that requires the check to be tamper-proof. The people it stops are not attacking anything - they are pasting a link to a friend.
What it does not prevent
The subscription response is a list of credentials. Once a client has fetched it, those credentials sit on the device in readable form: server address, port, UUID or password, Reality public key and short ID. Any client can use them without ever touching the subscription URL again - see how VLESS Reality is put together for what each field does.
It also helps to remember that the number of entries in a subscription is not the number of devices. A single server usually appears twice, once as a VLESS Reality profile and once as Hysteria2, because the two transports suit different networks - that comparison is in VLESS Reality against Hysteria2. Ten entries can be one location and one device.
- A config saved before the binding was refused keeps connecting, because the node checks the credential and knows nothing about headers.
- A client that sends no identifier cannot be bound. There is nothing to compare, so the fetch passes through unbound.
- The value is not tied to anything the server can verify, so two clients presenting the same string are one device as far as the endpoint is concerned.
- Two people using one config at the same time are limited by whatever the node enforces, which by default is nothing.
Enforcement that actually holds has to live where the tunnel terminates. In practice that means counting distinct source addresses per credential over a time window and disabling the account when the count is exceeded. The Xray core does not do this itself; panels built on top of it commonly implement it by parsing the access log, which is why the feature is approximate, and why it needs connection logging switched on at all - worth knowing, because that log records which address reached which account and when.
Why one phone can look like several devices
The endpoint compares two strings. It cannot distinguish «the same phone after an app reinstall» from «a second phone», because in both cases the only evidence is a value that changed. Ordinary events that produce a new identifier:
- Reinstalling the client, when the value lives in app storage, or when the app was the only one from its vendor on an iPhone.
- Clearing app data, in clients that store their own generated value. The OS-scoped Android ID survives that and changes only on a factory reset, so the outcome differs by client.
- Restoring a phone from a backup onto new hardware. The apps and the subscription link come back; a device-scoped identifier does not.
- A work profile or a second user on the same device - a separate namespace, therefore a separate identifier.
- A factory reset, an OS migration, or on desktop a disk or motherboard replacement.
- Two different clients on one machine, each fetching the same link. One phone, two devices, from the endpoint's point of view.
The mirror case is worth noticing too. A router with the config imported into it is exactly one device to the subscription endpoint, no matter how many phones, TVs and laptops sit behind it. The count is a count of clients that fetch the URL, not of things using the tunnel.
Counting by IP address fails differently
Where a provider counts concurrent addresses at the node instead of binding an identifier, the failure modes flip around. The signal is not client-supplied any more, but it is a poor proxy for «how many devices».
- Carrier-grade NAT puts many unrelated subscribers behind one address, so a shared address proves nothing about shared ownership.
- A dual-stack device can open connections over both IPv4 and IPv6 and appear in the log as two addresses at the same time.
- Moving from Wi-Fi to mobile data leaves the old TCP connection to time out, so one device holds two addresses until it does. QUIC-based transports handle the move better, but the node still records a new source address.
- Mobile networks rotate addresses on their own schedule, and a device that moved looks like a device that was added.
- One client opens many parallel connections from a single address, so counting connections rather than distinct addresses over-counts badly.
This is why an address-based limit needs a counting window and a grace period, and why a temporary disable is a safer response than an instant cut. A strict version of this rule locks out honest users on mobile networks every day.
When a legitimate device gets locked out
The usual sequence: you changed phones or reinstalled the client, the new installation fetches the same link, and the server refuses it because the old identifier is still on record. Deleting the profile in the app on the old device does not help - that deletion happens locally and the server never hears about it.
- 1Read what the client is actually showing. A binding refusal normally arrives as a subscription that imported successfully but contains one entry whose name explains the problem. A genuine network failure looks different: a timeout, a TLS error, or no response at all.
- 2Reset the binding from your account page. Dashboards call it reset device binding, unbind, or reset HWID.
- 3Press the refresh button in the client rather than waiting for the scheduled poll. The new identifier is recorded on the next fetch, not before.
- 4Make sure the old device is not still running the client. If it polls the subscription after your reset, it re-binds and locks out the new device - the reset frees the slot for whoever asks first.
- 5If two devices are permanent, ask for a separate subscription link for each instead of resetting back and forth. One token per device also makes it obvious which device broke when something does.
If you are the one implementing this
Notes for anyone building the same thing on their own panel, drawn from the failure modes above rather than from theory:
- Bind lazily on the first fetch and give the binding an expiry. A permanent binding turns every reinstall into a support ticket.
- Do not answer a refusal with a bare HTTP error. Most clients show nothing for a 403, and the user concludes the service is down.
- Give the user a self-service reset, rate-limited per account. Without it the reset becomes your most frequent manual operation.
- Never treat the header as an authentication factor. It authenticates nothing; the credential in the config does the real work.
- Prefer one token per device over one token per account. Revocation becomes surgical, logs become readable, and the whole reset dance disappears.
- Store the identifier hashed or truncated, and expire it with the subscription. It is a device identifier tied to a paying customer, and there is no reason to keep more of it than the comparison needs.
Common questions
Why does a new phone say the device limit is reached when I only have one device?
Because the server still holds the identifier of the previous installation. Removing the profile from the old phone, or the old phone itself, does not release the binding - that action is local and the server is never told. Reset the binding from your account page, then press refresh in the client on the new device.
Does resetting the device binding disconnect the device that was connected?
Usually not immediately. The binding governs the subscription fetch, while the tunnel authenticates with the UUID or password already stored in the client. The old device keeps connecting on its saved config until those credentials are rotated or the account is disabled. If a device needs to be genuinely cut off, the credentials have to change, not the binding.
Can one subscription cover a router and a phone at the same time?
To the subscription endpoint a router is one device: it fetches the link once, and everything behind it uses the tunnel without ever touching the URL. Counting source addresses at the node does not reveal more, because a router presents a single address regardless of how many devices sit behind it. Whether this is allowed is a policy question rather than a technical one. The phone is a second fetching client either way, so under a one-device rule it needs its own link.
Is HWID a hardware fingerprint that can identify me?
Not in the sense the name suggests. Sandboxed apps on current versions of iOS and Android cannot read hardware serial numbers, so the value is normally a per-installation or per-device identifier that changes on reinstall or factory reset. It is still an identifier your provider stores next to your subscription, which is a privacy consideration - just not a hardware one.
Why does the same link work in one client but hit the device limit in another?
Clients differ in two ways that matter here. Some send the identifier header and some never implemented it, and a client that sends nothing leaves the server nothing to compare. Clients also differ in how they refresh: one may still be serving a config list it fetched days ago and keep connecting happily, while a client that just polled the link gets the refusal. Check which client last updated the subscription before concluding the account is blocked.
The short version: a device limit is a rule about who may fetch a subscription link, enforced by comparing a string the client volunteers. It stops link sharing, which is what it exists for, and it regularly inconveniences people who did nothing more unusual than reinstall an app. If several devices are permanent, a separate link for each is less friction than a binding you keep resetting.