← back to blog
3 min read
  • #databases
  • #postgres
  • #uuid
  • #security

UUIDs vs auto-increment IDs

Sequential IDs let strangers guess your URLs and chart your growth. Here is when UUIDs earn their ugliness and when a plain serial column is honestly fine.


Your database hands out IDs in order. 1, 2, 3. That means anyone who sees invoice 4127 knows invoice 4126 exists, and can probably guess the URL for it. Whether that matters depends on what sits behind that URL, and I hit this question on every project now, so here is how I actually decide.

The enumeration problem

Say I build a booking page for a nail salon. Appointments live at /appointments/218. A bored customer edits the URL to /appointments/217 and reads someone else's name, phone number, and appointment time. If the server does not check ownership, that is a breach. The Security+ material I am studying calls this an insecure direct object reference, IDOR for short. It shows up constantly in bug bounty writeups because it is the easiest bug in the world to write.

Sequential IDs leak business data even when the authorization is solid. If a competitor signs up for your service and gets customer ID 341, then signs up again a month later and gets 379, they know you added 38 customers that month. The same trick works on order numbers, invoice numbers, and support tickets. This is old math. In World War II the Allies estimated German tank production from serial numbers on captured tanks, and the estimates beat the spy reports.

So a visible sequential ID gives away two things: that neighbors exist, and roughly how fast you are growing. For a landscaper with 40 clients, nobody is running that analysis. For a startup emailing invoices to customers who might talk to competitors, the invoice number is telling on you.

What UUIDs actually cost

A UUID looks like 8f3c1a52-9b7e-4c2d-a1f0-3d92e6b471aa. It is 128 bits, mostly random, which makes guessing a neighbor effectively impossible.

The costs are real though:

  • They are ugly. A salon owner can call and say booking 218 got double charged. Nobody dictates a UUID over the phone.
  • They are bigger. 16 bytes instead of 4 or 8, and every foreign key and every index pays the same tax.
  • Random UUIDs (version 4) insert all over the B-tree instead of appending at the end. On large tables that fragments the index and slows writes. Small tables will never notice.
  • Debugging gets slower. ORDER BY id no longer tells you insertion order.

UUIDv7 fixes the worst of it. The leading bits are a timestamp, so inserts stay roughly append-only and sort order roughly matches creation order. Postgres 18 ships a native uuidv7() function, and on older versions I generate them in application code. The tradeoff, and it is a real one, is that a v7 UUID leaks its own creation time to anyone who bothers to decode it. Usually harmless. Not always.

When serial is honestly fine

More often than the internet admits. My rule:

  • The ID never leaves the database or an internal admin page: bigint serial and move on. Join tables, logs, internal lookups. UUIDs there are pure overhead.
  • The ID appears in a URL, an email, a receipt, or an API response: UUIDv7, or a short random public code (an 8 character order reference works) sitting next to a serial primary key. The dual column approach keeps internal joins fast and hands the public something unguessable.

Supabase, which I use for my own app, already made this call on auth: user IDs are UUIDs out of the box. I keep that pattern for anything user facing.

One thing an unguessable ID does not buy you: authorization. If /appointments/8f3c1a52 returns data without checking that the logged in user owns the record, you still have IDOR. The ID is just harder to guess. Random IDs shrink the blast radius of that bug, they do not fix it. The ownership check is the security layer. The ID format is damage control.

The short version

Sequential IDs are fine until they are visible. Once an ID shows up in a URL or on a receipt, assume someone curious will increment it and someone patient will chart it. UUIDv7 for anything public, serial for anything private, and an ownership check on every single fetch either way.

I drill this stuff daily. IDOR and access control questions are all over the Security+ SY0-701 flashcards and quizzes I built into Aldo's Toolkit, free on both stores, no ads, no tracking. If you are studying too, grab it and see if you can outlast my daily question streak.

$ share

community rating

$ ls ./comments

sign in or create an account to rate and comment.

no comments yet, be first.