- Knowledge
- technology
- OOP
- Tips
- Programming
- Tips
- Tutorial
- SEO
- Ranking
- Knowledge
- Special Day
- Seo
- Bug
- Data science
- Seo
- artificial intelligence
- Machine Learning
- Robotics
- happyNewYear2021
- newYearEve
- 2021
- Automation
- Smart Home
- Career
- Best Practices
- Git
- Logging
- Web Fundamentals
- DNS
- HTTPS
- Performance
- AI Tools
- ChatGPT
- Claude
- Gemini
- Laravel
- Eloquent
- MySQL
- HTTPS
- TLS
- Web Security
- Certificates
- Developer Life
- Debugging
- Docker
- DevOps
- Transactions
- Queues
- LLMs
- AI
- AI Coding
- Developer Tools
- React Native
- Expo
- Kate PMS
- Mobile Apps
- Laravel
- Authentication
- Sanctum
- Cookies
- API Design
- Payments
- Idempotency
- DeepSeek
- Open Source AI
- LLMs
- AI News
- Git
- Version Control
- AI Coding
- Prompting
- PHP
- Checklist
- MCP
- AI Agents
- OpenAI
- Architecture
- Microservices
- Modular Monolith
- Estimation
- Developer Life
- Project Planning
- Humour
- OAuth
- OpenID Connect
- Authentication
- Embeddings
- Vector Search
- RAG
- pgvector
- OpenAI
- GPT-4.1
- Codex CLI
- Events
- Testing
- Clean Code
- Maintainability
- Code Review
- Webhooks
- API
- Security
- Claude Code
- Workflow
- AI
- LLM
- Prompt Injection
- Mobile
- React
- Networking
- TCP
- UDP
- HTTP/3
- CLAUDE.md
- AWS
- Cloud Security
- Backups
- PHPUnit
- Software Engineering
- Leadership
- Communication
- RAG
- Embeddings
- AI Engineering
- IT Infrastructure
- Networking
- Access Control
- CI/CD
- GitHub Actions
- Gemini CLI
- Claude Code
- JavaScript
- Async/Await
- Node.js
- Promises
- Security
- Cryptography
- Passwords
- MySQL
- Database
- Vibe Coding
- Software Quality
- DNS
- Code Reading
- Onboarding
- Productivity
- Background Jobs
- Developer Humour
- Estimates
- Dev Life
- JWT
- o3-mini
- DeepSeek R1
- Rate Limiting
- Kate PMS
- E-Signing
- Audit Trail
- REST
- GraphQL
- API Design
- Laravel 12
- Upgrade Guide
- Open Source
- Self-Hosting
- Task Scheduling
- Cron
- Secrets
- CORS
- PHP
- PHP-FPM
- OPcache
- GitHub Copilot
- Software Architecture
- Engineering
- TypeScript
- JavaScript
- Type Safety
- AI Security
- React Native
- Product Design
- AI Agents
- Kiro
- Queues
- Redis
- RabbitMQ
- AWS SQS
- Nginx
- Apache
- GPT-5
- gpt-oss
- Clean Code
- Architecture
- Naming
- Documentation
- Career
- ADR
- Teamwork
- Supply Chain
- Kate HRM
- HR Software
- Permissions
- System Design
- Pagination
- SSH
- Linux
- Big O
- Databases
- Laravel Boost
- MCP
- Developer Skills
- Validation
- Databases
- Indexes
- Code Quality
- Deployment
- Developer Humour
- Feature Flags
- Code Review
- Pull Requests
- Docker
- Cursor
- Authorization
- RBAC
- Gemini
- Long Context
- PHP 8.4
- Caching
- Dependency Injection
- Web Performance
- Browser
- CSS
- Database
- Migrations
- ChatGPT
- AI for Developers
- Monitoring
- On-Call
- REST
- Backend
- SQL
- NoSQL
- Database Design
- Coding Agents
- Claude 4
- API Resources
- REST API
- Load Balancing
- Scaling
- AWS
- AI Tools
- Claude
- Sora 2
- CTE
- 2FA
- TOTP
- Programming Languages
- Prompts
- Developer Workflow
- API Gateway
- APIs
- Passport
- API Auth
- Learning
- Burnout
- Developer Growth
- Web Development
- SEO
- Kate Mall
- ChatGPT Atlas
- Agent Skills
- Middleware
- Laravel 12
- Collections
- Context Window
- Monitoring
- Commit Messages
- Self Review
- Growth
- Regex
- Programming Basics
- Text Processing
- Database Design
- Normalization
- Linux
- Server Security
- Linux Foundation
- Open Standards
- Legacy Code
- Documentation
- AI Workflow
- File Uploads
- Test Data
- Hashing
- Performance
- Caching
- Enums
- Scope Creep
- Estimation
- Codex
- Gemini CLI
- Timezones
- Carbon
- Bugs
- PHP 8.5
- Gemini 3
- GPT-5.1
- Data Integrity
- Event Loop
- Async
- Opus 4.5
- AI Models
- React
- Forms
- Frontend
- Backups
- AI Images
- DALL-E
- Midjourney
- Race Conditions
- Concurrency
- Legacy Code
- Refactoring
- Senior Engineer
- Scope
- LLM
- CDN
- Web
- Sub-Agents
- Soft Deletes
- Audit Log
- Concurrency
- AI Learning
- NestJS
- AI Evals
- Policies
- SPF DKIM DMARC
- Unicode
- UTF-8
- Knowledge Graph
- Value Objects
- Technical Debt
- Feature Flags
- Laravel Pennant
- Deployment
- Copilot
- Composer
- Dependencies
- Artisan
- Automation
- AWS S3
- Object Storage
- Cloud
- Small Language Models
- Ollama
- Production
- Sessions
- HTTP
- Mentoring
- SQL
- Virtual Machines
- Web Development
- HTTP/2
- QUIC
- Web Performance
- AI Integration
- LLM API
- SOLID
- OOP
- Hosting
- Serverless
- Merge Conflicts
- Temperature
- AI Development
- Reverse Proxy
- Nginx
- Infrastructure
- Verification
- Passkeys
- WebAuthn
- Teams
- Communication
- Stakeholders
- Monorepo
- CI/CD
- Versioning
- JSON Schema
- Livewire
- Inertia
- Meetings
- Distributed Systems
- Privacy
- Full-Stack
- T-Shaped Skills
- Money
- Notifications
- Web Security
- HTTP Headers
- CSP
- Function Calling
- Load Testing
- k6
- Data Extraction
- Debugging
- WebSockets
- SSE
- Real-Time
- Laravel Reverb
- Infrastructure as Code
- Terraform
- Side Projects
- Laravel Pint
- OpenAPI
- Swagger
- UX
- Multimodal
- Jest
- Pair Programming
- APIs
- Rate Limiting
- Resilience
- Dev Humour
- Design Tokens
- JWT
- API Keys
- Sessions
- PHPStan
- Rector
- Incidents
- Reporting
- Dashboards
- Zero Trust
- IAM
- Search
- Laravel Scout
- Junior Developers
- Mentoring
- Images
- WebP
- AVIF
- Bug Reports
- Let's Encrypt
- Design Docs
- Software Design
- Observers
- Replication
- Accountability
- Data Structures
- Reliability
- LLM Memory
- Error Handling
- Payments
- Payment Gateway
- Webhooks
- PCI DSS
- Observability
- OpenTelemetry
- Personal Brand
- Writing
- Conventions
- Dates
- Scheduling
- Disaster Recovery
- Compression
- Brotli
- Deadlines
- Developer Habits
- State Machines
- Tech Roles
- UUID
- ULID
- Horizon
- Planning
- Engineering Culture
- Ownership
- Soft Skills
- Socialite
- Cost Control
- Collations
- Unicode
- Octane
- PostgreSQL
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-increment | UUIDv4 | UUIDv7 | ULID | |
|---|---|---|---|---|
| Generate without the DB | No | Yes | Yes | Yes |
| Insert-friendly ordering | Yes | No | Yes | Yes |
| Size | 8 bytes | 16 bytes (36 chars as text) | 16 bytes (36 chars as text) | 16 bytes (26 chars as text) |
| Guessable | Very | No | Partly (time prefix) | Partly (time prefix) |
| Leaks | Volume and growth | Nothing | Creation time | Creation time |
| Readable over the phone | Easily | No | No | Barely |
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?

Be first to comment it...