UUID generation can happen in three places: the browser or mobile client, the application server, or the database. Each has distinct trade-offs that interact with your consistency model, network architecture, and deployment topology. RFC 9562 defines UUID v4 and v7 but is silent on where generation should occur — that choice is yours. The Wikipedia UUID generation section summarizes the historical approaches; this guide focuses on the modern production trade-offs.
Strategy 1: Database-generated UUIDs
The database generates the UUID as a column default at insert time.
-- PostgreSQL 18+
CREATE TABLE events (
id uuid DEFAULT uuidv7() PRIMARY KEY,
data jsonb NOT NULL
);
-- Insert without specifying id
INSERT INTO events (data) VALUES ('{"type": "click"}');
-- The database assigns id; retrieve with RETURNING id
INSERT INTO events (data) VALUES ('{"type": "view"}') RETURNING id;
Advantages:
- Guaranteed to exist before application code processes the row
- No ID synchronization required between services
- PostgreSQL 18’s
uuidv7()includes a monotonic counter — strict per-millisecond ordering - Single generation point — no risk of client generating a malformed UUID
Disadvantages:
- You cannot know the ID before the INSERT — unsuitable for offline-first or optimistic UI
- Requires a database round trip before you can associate the ID with related in-memory objects
- Cross-database portability requires conditional migration logic (PostgreSQL 18 vs earlier versions)
Best for: Server-rendered applications, audit logs, event tables, any context where the application can always reach the database before needing the ID.
Strategy 2: Application-server-generated UUIDs
The application server generates the UUID before issuing the INSERT.
// Node.js — UUID v7 from uuid package
import { v7 as uuidv7 } from 'uuid';
async function createOrder(userId, totalCents) {
const id = uuidv7(); // generated before the INSERT
await db.query(
'INSERT INTO orders (id, user_id, total_cents) VALUES ($1, $2, $3)',
[id, userId, totalCents]
);
return id; // known before database round trip completes
}
# Python 3.13+ — UUID v7 from standard library
import uuid
def create_order(user_id: str, total_cents: int) -> str:
order_id = str(uuid.uuid7())
db.execute(
"INSERT INTO orders (id, user_id, total_cents) VALUES (%s, %s, %s)",
(order_id, user_id, total_cents)
)
return order_id
Advantages:
- ID is known before the INSERT — you can associate it with related objects immediately
- Works across any database version (no
uuidv7()function required) - Application controls generation quality (e.g., custom monotonic counter)
- ID can be included in an event message published before the INSERT commits (useful in event-driven architectures)
Disadvantages:
- Multiple application instances must each use a correctly-seeded CSPRNG
- No global monotonic ordering unless all generation is routed through a single process
- Application bugs can generate malformed or non-unique IDs (though this is extremely rare with a good library)
Best for: Microservices, event-driven systems, any application where the ID is needed before the database write.
Strategy 3: Client-generated UUIDs
The browser, mobile app, or CLI generates the UUID before sending a request to the server.
// Browser — UUID v4 (built-in, no install)
const id = crypto.randomUUID();
// Browser — UUID v7 (uuid package, sortable)
import { v7 as uuidv7 } from 'uuid';
const id = uuidv7();
// Mobile — iOS (Foundation)
import Foundation
let id = UUID().uuidString // UUID v4 — Foundation built-in
Advantages:
- Enables offline-first and optimistic UI — create the record locally before the network request
- Reduces server load by removing ID generation from the critical path
- Supports local-first sync architectures (e.g., Replicache, ElectricSQL)
Disadvantages:
- Server must validate that the UUID is well-formed before accepting it
- Malicious clients can submit any UUID — including one that collides with an existing record (though collision probability is negligible for v4/v7)
- UUID v7 timestamp is derived from the client’s clock — a skewed or manipulated clock produces incorrect ordering
- Client-generated UUIDs should never be trusted for security-critical operations without additional validation
Best for: Offline-first apps, collaborative editors, local-first sync databases, and any client that must create records without a server round trip.
Comparing clock sources for UUID v7
UUID v7’s timestamp quality depends on the source of Date.now() or its equivalent:
| Platform | Clock resolution | Notes |
|---|---|---|
| Node.js / Deno / Bun | 1 ms | High-resolution, system monotonic clock |
Browser (Date.now()) | 1–100 ms (jitter added) | Reduced resolution for fingerprinting protection — still sufficient for UUID v7 ordering |
PostgreSQL uuidv7() | 1 ms | Database wall clock — may differ from application server clock |
iOS Date() | Sub-millisecond | Date.now in Swift; NTP-synced by the OS |
Android System.currentTimeMillis() | 1 ms | NTP-synced by the OS |
Clock skew between nodes is addressed in UUID Clock Skew. For most applications, millisecond precision with a monotonic counter is sufficient — only high-frequency trading or strict event-ordering systems require sub-millisecond resolution.
Hybrid: client generates, server validates
Many production systems use a hybrid: the client generates a UUID and sends it with the request; the server validates it before using it:
// Server (Node.js / Express) — validate client UUID before INSERT
import { validate as uuidValidate, version as uuidVersion } from 'uuid';
app.post('/orders', async (req, res) => {
const { id, totalCents } = req.body;
// Validate format and version
if (!uuidValidate(id) || ![4, 7].includes(uuidVersion(id))) {
return res.status(400).json({ error: 'Invalid UUID' });
}
// Check for duplicates before INSERT
const existing = await db.query('SELECT id FROM orders WHERE id = $1', [id]);
if (existing.rows.length > 0) {
return res.status(409).json({ error: 'Duplicate ID' });
}
await db.query(
'INSERT INTO orders (id, total_cents) VALUES ($1, $2)',
[id, totalCents]
);
res.status(201).json({ id });
});
This gives clients the latency benefit of offline ID creation while the server maintains control over what enters the database.
Related reading
- UUID in JavaScript — crypto.randomUUID, uuid package, React patterns
- UUID in Databases — index behaviour, storage types, column defaults
- UUID Idempotency Keys — using client-generated UUIDs for safe retries
- UUID v4 vs v7 — sortability, security, and use-case guidance
External references
- MDN — Crypto.randomUUID() — browser-native UUID v4 generation
- uuid on npm — JavaScript UUID library with v4, v5, v7 support
- PostgreSQL 18 UUID functions — uuidv7() and gen_random_uuid() column defaults
- RFC 9562 — UUID specification — the IETF standard for UUID generation
Frequently asked questions
Should UUIDs be generated on the client or the server?
It depends on whether you need offline-first capability and whether you trust client-generated IDs. Clients can generate UUID v4 with crypto.randomUUID() or UUID v7 with the uuid npm package. Server generation is safer for audit trails and sequential ordering; database generation (e.g., PostgreSQL 18's uuidv7()) guarantees the ID exists before your application code sees it.
What is the benefit of generating UUIDs at the database layer?
The database becomes the single source of truth for ID generation. IDs are guaranteed to exist before any application code processes them, there is no risk of a client submitting a malformed or colliding UUID, and the generation function (e.g., uuidv7() in PostgreSQL 18) can include a database-level monotonic counter for strict per-millisecond ordering. The downside is that you cannot know the ID before the INSERT completes.
Can I generate UUID v7 on the client in a browser?
Yes. Use the uuid npm package's v7() function in any browser that supports Date.now() and crypto.getRandomValues() — which is all modern browsers. The uuid package uses a per-process monotonic counter for within-millisecond ordering. crypto.randomUUID() is built-in but only generates v4.