Profile    Mohammed Shiroz Status   Loading  
Logo
Share This
Back to blog
Filter by:
Tags
//Article title

UUID vs ULID vs Auto-Increment: Choosing IDs That Don't Race, Leak or Slow You Down

About Post

Open your app, look at the URL: /invoices/1043. Now change it to /invoices/1042.

If you see someone else's invoice, you have a security bug. If you see "403 Forbidden", you're safe, but you've still told the world roughly how many invoices you've issued, and that 1044 is coming next.

That small URL is where most teams first ask the question: should our IDs be auto-increment numbers, UUIDs or ULIDs? The honest answer is more interesting than "UUIDs are more secure", so let's go through it properly.

The three contenders in one minute

  • Auto-increment: the database hands out 1, 2, 3... Small, fast, readable, and only the database can generate them.
  • UUID: a 128-bit identifier like 0190f1d2-7c4a-7b3e-9f21-3a8c5e6d4b10. Version 4 is fully random. Version 7 starts with a timestamp, so new ones sort after old ones.
  • ULID: also 128 bits, written as 26 characters like 01J3Z8Q6K9V2T5X7M4N8P0R1SW. The first part is a millisecond timestamp, the rest is random. Sortable, URL-friendly, a bit shorter as text.

What "race-free" really means here

Auto-increment is perfectly safe inside a single database. Two inserts at the same instant still get different numbers. The catch is who can create an ID and when.

With auto-increment, you only learn the ID after the insert. That's awkward in a few common situations:

  • Offline mobile apps. A staff app that creates a record without signal needs an ID now, to link photos and notes to it, long before it can talk to the server.
  • Idempotent creates. If the client generates the ID, a retried request with the same ID can't create a duplicate.
  • Merging data. Combining records from two databases, or importing from another system, with auto-increment means collisions and remapping.
  • Events before inserts. Sometimes you want to queue a job or publish an event referencing a record in the same breath as creating it.

UUIDs and ULIDs can be generated anywhere: the browser, the phone, any app server, with no coordination. The chance of two random 128-bit values colliding is so small that, in practice, you treat them as unique. That's the "race-free" part: no central counter to race for.

The performance part people skip

In MySQL's InnoDB, the table is physically stored in primary key order (a clustered index). That has two consequences that matter for ID choice.

1. Random keys scatter your inserts. Auto-increment always appends to the end of the index, which is cheap. A random UUIDv4 lands somewhere random in the middle, so the database keeps splitting pages and touching cold parts of the index. On a small table you won't notice. On a big, busy one, you will.

2. Every secondary index carries the primary key. In InnoDB, each secondary index entry stores the primary key value as its pointer to the row. A 36-character string key makes every index on the table bigger than an 8-byte integer would.

This is exactly why the time-ordered formats exist. UUIDv7 and ULID start with a timestamp, so new IDs land near the end of the index, much like auto-increment. You keep the "generate anywhere" property and lose most of the insert penalty.

Side by side

Auto-incrementUUIDv4UUIDv7ULID
Generate without the DBNoYesYesYes
Insert-friendly orderingYesNoYesYes
Size8 bytes16 bytes (36 chars as text)16 bytes (36 chars as text)16 bytes (26 chars as text)
GuessableVeryNoPartly (time prefix)Partly (time prefix)
LeaksVolume and growthNothingCreation timeCreation time
Readable over the phoneEasilyNoNoBarely

Note the "Leaks" row. Time-ordered IDs carry their creation time inside them. Usually harmless, occasionally not: if the creation time of a record is sensitive, a UUIDv7 in a public URL gives it away.

Unguessable is not the same as secure

Back to /invoices/1042. Switching to UUIDs makes IDs hard to guess, and that's a real benefit. But if your app shows an invoice to anyone who has its ID, you still have the bug. IDs leak through emails, logs, screenshots and browser history.

The rule: authorisation decides who can see a record. The ID format only decides how hard it is to find one. Always do the policy check, whatever the ID looks like.

How Laravel makes this easy

Laravel has traits for both. In Laravel 12, HasUuids generates time-ordered UUIDv7 values (use HasVersion4Uuids if you specifically want random v4), and HasUlids generates ULIDs:

Schema::create('maintenance_requests', function (Blueprint $table) {
    $table->ulid('id')->primary();
    $table->foreignUlid('unit_id')->constrained();
    $table->string('title');
    $table->timestamps();
});

class MaintenanceRequest extends Model
{
    use HasUlids;
}

The model generates the ID on creation, and a client-provided ID works too, which is what makes the offline mobile case possible (validate its format before you accept it).

The hybrid I usually reach for

You don't have to pick one ID for every job. A common, very practical pattern: keep a fast bigint primary key for joins and foreign keys inside the database, and add a separate public ID for URLs and APIs.

class Invoice extends Model
{
    use HasUlids;

    public function uniqueIds(): array
    {
        return ['public_id']; // generate a ULID here, not in "id"
    }

    public function getRouteKeyName(): string
    {
        return 'public_id';
    }
}

Add a unique public_id column in the migration, and route model binding now resolves /invoices/01J3Z8Q6... while your foreign keys stay small integers. Internal joins stay cheap; the outside world never sees a counter.

So which should you use?

  • Internal tables nobody sees: auto-increment is still a great default.
  • IDs in public URLs or APIs: a ULID or UUIDv7, either as the primary key or as a separate public column.
  • Clients that create records offline, or data merged from several sources: ULID or UUIDv7 as the primary key.
  • Random UUIDv4 as a primary key on a large, busy table: avoid, unless you have a specific reason.

And whatever you choose, pick one convention per project and stick to it. Mixed ID formats across tables are a small, constant tax on everyone's attention.

What does your current project use for public IDs, and would you choose the same again if you started today?

Comments (0)
Leave your review

Thanks for your valuable comments. Your comments has been updated and appreciate your getting in touch...

01. About Shiroz

Mohammed Shiroz

Hi, I'm Mohammed Shiroz, a software engineer and AI enthusiast from Sri Lanka who turns ideas into intelligent, real-world solutions. With over 9 years of hands-on experience, I currently lead real estate ERP development at Kate Group, a...

03.My Projects

04. Categories

Ready To order Your Project ?

Get in Touch
Close