-
Notifications
You must be signed in to change notification settings - Fork 2
ID SequenceID
Database-friendly sequential 64-bit integer ID generator for distributed systems.
- Overview
- Features
- ID Structure
- Installation
- API Reference
- Usage Examples
- Use Cases
- Database Integration
- Best Practices
- MariaDB UUID_SHORT() Comparison
- Performance
- Related Documentation
SequenceID generates 64-bit integers suitable for database primary keys, modeled directly on MariaDB's UUID_SHORT() function. Each ID combines a process-derived server ID, the generator's startup time, and a monotonic counter — no random component.
The generated IDs are:
- Sequential: Monotonically increasing for optimal B-tree index performance
- Server-aware: Incorporates process ID to discriminate across instances
-
Database-friendly: 64-bit integers compatible with
BIGINTcolumns - Traceable: Embeds server and startup-time information for debugging
Why SequenceID?
- Better index performance: sequential IDs cluster well in B-tree indexes; random UUIDs cause page splits.
- Smaller storage: 8 bytes vs 16 bytes for UUID.
-
Native database type: standard
BIGINT, no special UUID columns needed. - No database round-trip: generated entirely client-side.
Uniqueness contract. SequenceID is collision-free within a single generator for up to 16,777,216 IDs per startup-second. It is not collision-free across multiple generators in the same process, nor across multiple processes with colliding
PID % 256started in the same wall-clock second. TreatsequenceID()as a singleton per logical sequence (e.g. one per table) and instantiate it at module load. For distributed/clustered scenarios, preferulidorObjectID.
Security note: SequenceID has no random component — server ID, startup time, and counter are all fully predictable to anyone who can observe a few consecutive IDs. Never use it for session tokens, password-reset links, API keys, or anywhere the value must be hard to guess — use NanoID, ulid, or cuid2 for those.
| Feature | Support | Description |
|---|---|---|
| 64-bit Integer Output | ✅ | Native BIGINT support in all major databases |
| Sequential Generation | ✅ | Monotonically increasing for index optimization |
| Server-Aware | ✅ | Embeds process ID to discriminate cross-instance |
| Timestamp Component | ✅ | Includes startup time for temporal ordering |
| Counter Management | ✅ | Internal 24-bit counter with override capability |
| Dependency-light | Uses @tundralibs/compat for the process ID |
|
| Collision Resistant (in-process) | ✅ | Up to 16,777,216 IDs/sec/generator |
| Database Optimized | ✅ | Sequential nature improves B-tree index performance |
| Distributed Systems | Caller must ensure unique (PID % 256, startup_sec)
|
SequenceID generates a 64-bit integer composed of three components — this matches MariaDB UUID_SHORT() exactly:
┌─────────────┬──────────────────────┬──────────────────────┐
│ Server ID │ Startup Time │ Counter │
│ (8 bits) │ (32 bits) │ (24 bits) │
└─────────────┴──────────────────────┴──────────────────────┘
0-255 Unix seconds 0 - 16,777,215
Component Breakdown:
-
Server ID (8 bits, bits 56-63):
getProcessId() % 256(or0if PID unavailable).- Discriminates processes by PID.
- PIDs that share a residue mod 256 (e.g., 1 and 257) will collide on this field.
-
Startup Time (32 bits, bits 24-55): Unix epoch seconds at generator construction.
- Set once per
sequenceID()call; constant for the generator's lifetime. - Two generators created in the same wall-clock second share this value.
- Set once per
-
Counter (24 bits, bits 0-23): Per-generator monotonic counter.
- Starts at
seed(default0), increments on each call. - Safe range: 16,777,216 IDs per startup-second. Past this, the counter spills into the startup-time bits and may collide with values from a hypothetical generator created in a later second.
- Can be reset mid-stream via the
gen(N)override; this becomes the new value.
- Starts at
Example ID Decomposition:
ID: 72623859790382856n
Server ID: 1 (Process ID % 256)
Startup Time: 1713849600 (April 23, 2024 00:00:00 UTC)
Counter: 8 (8th call to this generator)Deno:
deno add @tundralibs/idBun:
bunx jsr add @tundralibs/idNode.js:
npx jsr add @tundralibs/idDirect import (Deno):
import { sequenceID } from 'jsr:@tundralibs/id';Creates a database-friendly sequential ID generator.
function sequenceID(cnt?: number): (counter?: number) => bigint;Parameters:
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
cnt |
number |
No | 0 |
Initial counter value (non-negative integer) |
Returns:
A generator function that produces unique 64-bit integer IDs.
Throws:
-
InvalidOptionError- If initial counter value is negative or not an integer (NaN, a fractional value, or Infinity)
Example:
import { sequenceID } from '@tundralibs/id';
const generator = sequenceID();
const id = generator(); // 72623859790382856nThe returned generator function can be called to produce IDs.
type GeneratorFunction = (counter?: number) => bigint;Parameters:
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
counter |
number |
No | Internal counter | Override counter value (non-negative integer) |
Returns:
A unique 64-bit bigint ID.
Throws:
-
InvalidOptionError- If counter override value is negative or not an integer (NaN, a fractional value, or Infinity)
Behavior:
- Without argument: Uses and increments internal counter
- With argument: Resets internal counter to provided value
- Counter increments without wrapping; it is collision-free within the 24-bit range (0-16,777,215) per startup-second, after which it spills into the startup-time bits (see ID Structure)
Generate sequential IDs for database records:
import { sequenceID } from '@tundralibs/id';
// Create a generator
const idGen = sequenceID();
// Generate IDs
const id1 = idGen(); // 72623859790382856n
const id2 = idGen(); // 72623859790382857n
const id3 = idGen(); // 72623859790382858n
console.log(id2 - id1); // 1n (sequential)Start counting from a specific value:
import { sequenceID } from '@tundralibs/id';
// Start counter at 1000
const idGen = sequenceID(1000);
const id1 = idGen(); // Counter: 1000
const id2 = idGen(); // Counter: 1001
const id3 = idGen(); // Counter: 1002Reset counter mid-sequence for specific scenarios:
import { sequenceID } from '@tundralibs/id';
const idGen = sequenceID();
const id1 = idGen(); // Uses internal counter: 0
const id2 = idGen(); // Increments to: 1
const id3 = idGen(5000); // Override to: 5000
const id4 = idGen(); // Continues from: 5001
console.log((id2 & 0xFFn) - (id1 & 0xFFn)); // 1n
console.log((id4 & 0xFFn) - (id3 & 0xFFn)); // 1nSafe ID generation across multiple servers:
import { sequenceID } from '@tundralibs/id';
// Server 1 (Process ID: 1234)
const server1Gen = sequenceID();
const server1Id = server1Gen(); // Includes server info
// Server 2 (Process ID: 5678)
const server2Gen = sequenceID();
const server2Id = server2Gen(); // Different server component
// Extract server ID from generated ID
const extractServerId = (id: bigint): bigint => {
return (id >> 56n) & 0xFFn;
};
const server1Info = extractServerId(server1Id); // 1234 % 256
const server2Info = extractServerId(server2Id); // 5678 % 256Replace auto-increment with globally unique sequential IDs:
import { sequenceID } from '@tundralibs/id';
const userIdGen = sequenceID();
interface User {
id: bigint;
username: string;
email: string;
}
function createUser(username: string, email: string): User {
return {
id: userIdGen(),
username,
email,
};
}
const user = createUser('alice', 'alice@example.com');
console.log(user.id); // 72623859790382856nEliminate database coordination for distributed ID generation:
import { sequenceID } from '@tundralibs/id';
// Traditional auto-increment requires database lock
// SequenceID works without database coordination
const orderIdGen = sequenceID();
class Order {
id: bigint;
customerId: string;
total: number;
constructor(customerId: string, total: number) {
this.id = orderIdGen(); // No database roundtrip
this.customerId = customerId;
this.total = total;
}
}
const order = new Order('cust_123', 99.99);Optimize database inserts with sequential IDs:
import { sequenceID } from '@tundralibs/id';
const logIdGen = sequenceID();
interface LogEntry {
id: bigint;
timestamp: Date;
level: string;
message: string;
}
// Batch insert with pre-generated IDs
function batchInsertLogs(entries: Omit<LogEntry, 'id'>[]): LogEntry[] {
return entries.map((entry) => ({
id: logIdGen(),
...entry,
}));
}
const logs = batchInsertLogs([
{ timestamp: new Date(), level: 'INFO', message: 'App started' },
{ timestamp: new Date(), level: 'DEBUG', message: 'Config loaded' },
{ timestamp: new Date(), level: 'INFO', message: 'Server listening' },
]);Using SequenceID with PostgreSQL BIGINT columns:
-- Create table with BIGINT primary key
CREATE TABLE users (
id BIGINT PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(100) UNIQUE NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Index on sequential IDs is highly efficient
CREATE INDEX idx_users_id ON users(id);import { sequenceID } from '@tundralibs/id';
import { Pool } from 'pg';
const pool = new Pool({
connectionString: 'postgresql://localhost/mydb',
});
const userIdGen = sequenceID();
async function createUser(username: string, email: string) {
const id = userIdGen();
await pool.query(
'INSERT INTO users (id, username, email) VALUES ($1, $2, $3)',
[id.toString(), username, email],
);
return id;
}
// Usage
const userId = await createUser('alice', 'alice@example.com');
console.log(`Created user with ID: ${userId}`);Compatible with MySQL/MariaDB BIGINT UNSIGNED:
-- Create table with BIGINT UNSIGNED primary key
CREATE TABLE orders (
id BIGINT UNSIGNED PRIMARY KEY,
customer_id VARCHAR(50) NOT NULL,
total DECIMAL(10, 2) NOT NULL,
status ENUM('pending', 'completed', 'cancelled') DEFAULT 'pending',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_customer_id (customer_id),
INDEX idx_created_at (created_at)
) ENGINE=InnoDB;import { sequenceID } from '@tundralibs/id';
import mysql from 'mysql2/promise';
const connection = await mysql.createConnection({
host: 'localhost',
user: 'root',
database: 'mydb',
});
const orderIdGen = sequenceID();
async function createOrder(customerId: string, total: number) {
const id = orderIdGen();
await connection.execute(
'INSERT INTO orders (id, customer_id, total) VALUES (?, ?, ?)',
[id.toString(), customerId, total],
);
return id;
}
// Usage
const orderId = await createOrder('cust_123', 99.99);
console.log(`Created order with ID: ${orderId}`);Use separate generators for different entity types:
import { sequenceID } from '@tundralibs/id';
// One generator per entity type
const userIdGen = sequenceID();
const orderIdGen = sequenceID();
const productIdGen = sequenceID();
// Don't share generators across entity typesConvert to string for JSON serialization:
import { sequenceID } from '@tundralibs/id';
const idGen = sequenceID();
const id = idGen();
// Store as string in JavaScript objects
const user = {
id: id.toString(), // "72623859790382856"
username: 'alice',
};
// Parse back to bigint when needed
const parsedId = BigInt(user.id);Always use BIGINT (or BIGINT UNSIGNED) for ID columns:
-- PostgreSQL
CREATE TABLE records (
id BIGINT PRIMARY KEY
);
-- MySQL/MariaDB
CREATE TABLE records (
id BIGINT UNSIGNED PRIMARY KEY
);
-- Don't use INT or VARCHARThe counter occupies the low 24 bits (0-16,777,215) and increments without wrapping — it does not wrap at 255. Within one startup-second a single generator stays collision-free for up to 16,777,216 IDs; past that the counter spills into the startup-time bits and may collide with a generator created in a later second:
import { sequenceID } from '@tundralibs/id';
const idGen = sequenceID(250);
// The counter increments monotonically — it does NOT wrap at 255.
const id250 = idGen(); // Counter: 250
const id251 = idGen(); // Counter: 251
// ... continues up to 16,777,215 within the 24-bit safe range, then spills over.For workloads that emit more than ~16M IDs per startup-second from one
generator, switch to ulid or ObjectID.
Ensure system clocks are synchronized:
# Use NTP to synchronize clocks
sudo systemctl enable --now systemd-timesyncd
# Verify synchronization
timedatectl statusWhile extremely rare, monitor for duplicate IDs:
import { sequenceID } from '@tundralibs/id';
const idGen = sequenceID();
const generatedIds = new Set<string>();
function generateUniqueId(): bigint {
const id = idGen();
const idStr = id.toString();
if (generatedIds.has(idStr)) {
console.error('Collision detected!');
// Implement collision handling
}
generatedIds.add(idStr);
return id;
}Leverage sequential nature for index optimization:
-- B-tree indexes work great with sequential IDs
CREATE INDEX idx_sequential ON records(id);
-- Avoid hash indexes for range queries
-- HASH indexes don't benefit from sequential natureSequenceID mirrors MariaDB's UUID_SHORT() bit layout exactly:
| Aspect | SequenceID | MariaDB UUID_SHORT() |
|---|---|---|
| Output Type | 64-bit bigint | 64-bit unsigned bigint |
| Server ID |
getProcessId() % 256 (8 bits) |
server_id config (8 bits) |
| Timestamp | Startup time, seconds (32 bits) |
server_startup_time (32 bits) |
| Counter | 24-bit, per-generator | 24-bit, per-server |
| Uniqueness model | Per-generator (caller singleton) | Per-server (DB coordinates) |
| Generation site | Client (no DB roundtrip) | Server (one round-trip per ID) |
| Portability | Any JS runtime | MariaDB only |
Key Differences:
-
Counter scope: MariaDB shares a single counter across the whole server; SequenceID's counter lives in the generator closure. Multiple
sequenceID()instances in one process do not share state — they will produce identical IDs if created in the same startup-second. Treat eachsequenceID()as a singleton per logical sequence. - Client-side generation: no database query needed.
-
Process-based server ID: uses
getProcessId() % 256instead of a configuredserver_id. Cluster operators are responsible for ensuring PIDs don't collide modulo 256 across nodes (or for accepting the collision probability). -
Runtime agnostic: works on Deno, Bun, Node, Cloudflare Workers, and browsers — the process-derived server ID falls back to
0where no PID is exposed (Workers, browsers).
When to use SequenceID over UUID_SHORT():
- Need client-side ID generation without a database round-trip.
- Using PostgreSQL, MongoDB, or any non-MariaDB database.
- Want portable code across databases.
- Single-process or low-process-count deployment where a
(PID % 256, startup_sec)collision is unlikely.
When to use UUID_SHORT() instead:
- Already on MariaDB with
server_idconfigured per node in a cluster. - Need a guaranteed-unique counter coordinated by the database.
When to use ULID or ObjectID instead:
- Distributed deployments where multiple nodes may share
PID % 256. - Workloads emitting more than ~16M IDs per startup-second per generator.
- Any context where collision must be cryptographically improbable rather than schedule-controlled.
SequenceID is optimized for high-throughput generation:
import { sequenceID } from '@tundralibs/id';
// Benchmark: Generate 1 million IDs
const idGen = sequenceID();
const iterations = 1_000_000;
console.time('Generate 1M IDs');
for (let i = 0; i < iterations; i++) {
idGen();
}
console.timeEnd('Generate 1M IDs');
// Typical: ~50-100ms on modern hardwarePerformance Characteristics:
- Generation Speed: ~10-20 million IDs/second (single-threaded)
- Memory Overhead: Minimal (one generator instance)
- CPU Usage: Very low (simple bit operations)
- Database Impact: None (client-side generation)
sequenceID touches no CSPRNG on the hot path, which makes it the fastest
generator in this package. For measured per-generator numbers — including how
it compares to ObjectID, simpleID, cuid2, ulid, nanoID, and cuid on
Deno, Bun, and Node — see Performance.
- ID Generator Overview - Main documentation for all ID generators
- NanoID - URL-safe string IDs
- ObjectID - MongoDB-compatible IDs
- ULID - Lexicographically sortable IDs
- SimpleID - Minimal unique IDs