Quick answer
Yes, for random v4 UUIDs, as long as your app still checks on every request that the user is allowed to see the record. A UUID hides how many records you have and cannot be guessed by counting. But it is not a secret: URLs end up in logs, browser history and shared messages. Avoid v1 in public URLs, and remember that v7 shows when the record was created.
What a UUID can reveal
Each UUID version is built from different inputs, and some of those inputs can be read back out. Paste any ID into the UUID decoder to see what it contains.
| Version | What someone can learn from it |
|---|---|
| v1 | When it was made, to a fraction of a microsecond. Often the MAC address of the machine that made it. |
| v6 | When it was made. The MAC address too, if the library keeps the old v1 behaviour. |
| v7 | When it was made, to the millisecond. |
| v3, v5 | Nothing directly. But anyone who can guess the input name can compute the same ID. |
| v4 | Nothing. It is random. |
| v8 | Whatever the app chose to put in it. |
The v3 and v5 case catches people out. These versions hash a namespace and a name, so the same name always gives the same UUID. If you made user IDs from email addresses with v5, anyone who knows an email address and your namespace can work out that user’s ID.
The v7 case is milder but real. A URL like /orders/01a16818-85c0-7b3e-9f2a-4c6d81e05a73 tells anyone who sees it that the order was placed on 23 October 2026 at 09:30 UTC.
A UUID is not a password
RFC 9562, the UUID standard, is direct about this. Its security section says implementations “SHOULD NOT assume that UUIDs are hard to guess”, and that UUIDs “MUST NOT be used as security capabilities”, meaning IDs whose mere possession grants access.
Even a perfectly random v4 leaks easily once it is in a URL. It gets copied into:
- web server, proxy and CDN logs
- browser history and bookmarks
- analytics and error tracking tools
- screenshots, chat messages and support tickets
- the
Refererheader sent to other sites, if your referrer policy sends the full URL
So /invoices/<uuid> must still check that the person asking owns that invoice. If it does not, anyone who sees the link can open it.
A v4 is also only as random as the code that made it. crypto.randomUUID() in browsers and Node.js, and uuid.uuid4() in Python, use a secure random source. Home-made generators built on Math.random() do not.
What UUIDs do well
Compared with plain numbers, UUIDs in URLs have real benefits:
- No counting.
/orders/1042tells a competitor roughly how many orders you have, and invites someone to try/orders/1043. A random UUID gives nothing away. - No coordination. Clients and services can make IDs before they talk to the database.
- URL-friendly. Hex digits and hyphens never need escaping.
What they do not fix is broken access control. Security people call it IDOR (insecure direct object reference): the app returns a record because the ID matched, without checking who asked. Hard-to-guess IDs make that bug harder to find, not gone.
When to use something else
Password resets, email links, magic logins and share links. Use a dedicated random token, not a record ID. Make it at least 32 random bytes, store only a hash of it, and give it an expiry date so it can stop working.
// Node.js
import { randomBytes } from 'node:crypto';
const token = randomBytes(32).toString('base64url');
# Python
import secrets
token = secrets.token_urlsafe(32)
Short or readable URLs. A UUID is 36 characters. If that is too long, see UUID vs ULID vs Nano ID, or use a slug for pages people will type or share.
When creation time is private. Use v4 for the public ID, even if you use v7 as the internal primary key.
A safe setup
- Make public IDs with a secure v4 generator
crypto.randomUUID() - Check the format before touching the database, and return 404 if it is wrong
- Look the record up together with the current user
SELECT * FROM orders WHERE id = $1 AND user_id = $2 - Return the same 404 for "not found" and "not yours"
- Pick one form, lowercase with hyphens, and redirect other forms to it
Returning the same response in step 4 matters. If “not yours” gave a 403 instead, anyone could use your site to test which IDs exist. For the format check, see how to check if a string is a valid UUID.

