Snowflaked
A database-agnostic, high-performance, thread-safe Snowflake ID generator for Ruby, powered by Rust.
Snowflake IDs are 64-bit unique identifiers that encode a timestamp, machine ID, and sequence number. They're time-sortable (IDs created later are always larger), making them ideal for distributed systems where you need unique IDs without coordination between machines. Unlike UUIDs, Snowflake IDs are smaller, sortable, and index-friendly for databases.
Installation
Add to your Gemfile:
gem "snowflaked"Quick Start
id = Snowflaked.id
# => 7193489234823847936Rails Integration
All models automatically generate a Snowflake ID for the :id attribute:
class User < ApplicationRecord
end
User.create!
# => #<User id: 7193489234823847936>You can also define additional Snowflake columns in migrations:
class CreateUsers < ActiveRecord::Migration[8.1]
def change
create_table :users do |t|
t.snowflake :external_id
t.bigint :uid
end
end
endColumns created with t.snowflake are automatically detected and will have Snowflake IDs generated for them.
Note
IDs are assigned in a before_create callback, so they are not available during validations and are not generated by bulk operations that skip callbacks (insert_all, upsert_all). For those, generate IDs yourself with Snowflaked.id.
Warning
SQLite does not support column comments, which Snowflaked uses to auto-detect snowflake columns other than :id. When using SQLite, you must explicitly declare snowflake columns using the snowflake_id helper in your model.
If you want to generate Snowflake IDs for additional columns, you can do so by using the snowflake_id method, without having to migrate the table:
class User < ApplicationRecord
snowflake_id :uid
endIt is also possible to disable automatic :id generation by passing id: false to the snowflake_id method:
class Post < ApplicationRecord
snowflake_id id: false
endOr generate Snowflake IDs for other columns but not :id:
class Post < ApplicationRecord
snowflake_id :external_id, id: false
endConfiguration
Snowflaked.configure do |config|
config.machine_id = 42
config.epoch = Time.utc(1989, 1, 3) # Defaults to DEFAULT_EPOCH (2024-01-01) if not configured
endConfiguration locks after you call Snowflaked.configure, or after the first generated or parsed ID. Set your configuration during application boot. Do this before you start request threads.
If you use Ractors, set your configuration before you start any other Ractor. See Ractor Safety below for the reason.
Machine ID
Tip
For multi-process servers like Puma in cluster mode, it is recommended to not configure machine_id explicitly. The gem derives a process-local machine ID after each fork, so every worker gets its own value.
Warning
Never share one machine_id between concurrently running processes. Each process keeps its own sequence counter, so two processes with the same machine ID that generate in the same millisecond can produce identical IDs. In particular, a fixed SNOWFLAKED_MACHINE_ID (or machine_id in the initializer) on a host running Puma in cluster mode gives every worker the same machine ID.
Auto-detection is effectively a random value between 0 and 1023 per process. With only 1024 possible values, collisions follow birthday math: at ~40 concurrent processes there is a ~50% chance that two of them share a machine ID, and beyond 1024 processes a collision is guaranteed. A shared machine ID only yields a duplicate ID when both processes generate in the same millisecond with the same sequence number — unlikely, but real at scale. Duplicates on :id are caught by the primary key (ActiveRecord::RecordNotUnique); duplicates on other snowflake columns are silent unless the column has a unique index.
If your fleet is large or duplicate IDs are unacceptable, assign explicit per-process machine IDs (e.g. derived from a pod index plus a worker index).
Machine ID Resolution
If machine_id is not explicitly configured, it resolves in this order:
-
SNOWFLAKED_MACHINE_IDenvironment variable -
MACHINE_IDenvironment variable - Auto-detected: a process-local value, effectively random between
0and1023
For Kubernetes deployments, only set an explicit machine ID if each pod runs a single generating process (e.g. Puma in single mode). A StatefulSet ordinal or pod index is then a good source of unique values:
env:
- name: SNOWFLAKED_MACHINE_ID
valueFrom:
fieldRef:
fieldPath: metadata.annotations['apps.kubernetes.io/pod-index']With Puma in cluster mode, leave machine_id unset and let each worker derive its own.
Clock Safety
Snowflake IDs require a monotonically increasing clock. If the system clock steps backwards (NTP correction, VM migration), Snowflaked.id raises a RuntimeError instead of risking duplicate IDs, and keeps raising until the clock catches up with the last generated timestamp. Run NTP in slew mode (the default on most systems) to avoid backward steps.
Ractor Safety
This gem works inside a Ruby Ractor. CI runs the audition gem on every change, to check this.
One rule applies. Call Snowflaked.configure, or generate the first ID, on the main Ractor. Do this before you start any other Ractor. Snowflaked::Configuration must stay open to change: it re-derives machine_id after each fork. For this reason, only the main Ractor can set it up safely, and every other Ractor must find it already set up.
API Reference
id = Snowflaked.id
Snowflaked.parse(id)
# => {timestamp_ms: 1735123456789, machine_id: 42, sequence: 0}
Snowflaked.timestamp(id)
# => 2024-12-25 12:34:56 +0000
Snowflaked.timestamp_ms(id)
# => 1735123456789
Snowflaked.sequence(id)
# => 0
Snowflaked.machine_id(id)
# => 42Benchmarks
See BENCHMARKS.md for more details.
tl;dr: Snowflake IDs have a negligible performance impact compared to database-backed IDs.
Requirements
- Ruby >= 3.3
- rustc / cargo >= 1.81.0 (development uses the toolchain pinned in
mise.toml) - Mise
Development
mise install
bundle install
bundle exec rakeAcknowledgments
- snowflaked-rs - the Rust implementation of Snowflake IDs
License
MIT