Skip to content

Conversation

@ryanwinchester
Copy link
Contributor

@ryanwinchester ryanwinchester commented Nov 21, 2025

Summary

Now that we have UUIDv7 (#4681):

Add monotonicity support and sub-millisecond precision to UUIDv7 generation, ensuring strictly time-ordered UUIDs even under high-throughput scenarios.

Sub-millisecond1 UUID generation uses increased clock precision (Method 3) from RFC-9562 Section 6.2.

Additions/Changes

  • Updated Ecto.UUID.generate/1 options - Extended the options type to include :precision and :monotonic options alongside the existing :version option.
  • Monotonic UUIDv7 generation - New monotonic: true option ensures UUIDs are strictly monotonically increasing, even when multiple UUIDs are generated within the same timestamp. Uses atomic counters stored in persistent terms for safe concurrent generation.
  • Sub-millisecond precision1 - New precision: :nanosecond option encodes sub-millisecond precision in the rand_a field, allowing up to 4096 distinct values per millisecond and reducing timestamp drift under high throughput.
  • Application startup - Added initialization of atomic counters in Ecto.Application.start/2 for both millisecond and nanosecond precision tracking.

Footnotes

  1. derived from nanoseconds 2

@ryanwinchester ryanwinchester force-pushed the feature/uuidv7-monotonicity branch 11 times, most recently from 206cd67 to a864137 Compare November 21, 2025 21:59
lib/ecto/uuid.ex Outdated
# backbone of time-based sortable UUIDs. Normally, time-based UUIDs will be
# monotonic due to an embedded timestamp; however, implementations can
# guarantee additional monotonicity via the concepts covered in section 6.2.
case Keyword.get(opts, :monotonic_method) do
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am thinking they are rather two different arguments:

precision: :millisecond | :nanosecond
monotonic: true | false

WDYT?

Copy link
Contributor Author

@ryanwinchester ryanwinchester Nov 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I like those names much better. I hated the arguments I came up with and couldn't think of better

However, we should note that it's not actually nanoseconds, but fractions of a millisecond (derived from nanoseconds).

We have 12 bits of extra precision and can only fit 4096 values (out of 1_000_000 nanoseconds per millisecond).

The monotonic version always steps each UUID by at least 244.

minimum_step = nanoseconds_per_milliseconds / available_values

# where available_values is the number of values available in 12 bits, 2^12 (4096).

1_000_000 / 4096 #=> 244

Copy link
Contributor

@dkuku dkuku Nov 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wdyt about mode: monotonic or method ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wdyt about mode: monotonic or method ?

i think it could be good if we were going to implement other optional methods (like counters)

however, I don't think it's worth implementing more than one method

@ryanwinchester ryanwinchester force-pushed the feature/uuidv7-monotonicity branch 5 times, most recently from 4bc8da8 to f0a6346 Compare November 23, 2025 16:51
@ryanwinchester ryanwinchester marked this pull request as ready for review November 23, 2025 17:02
@ryanwinchester ryanwinchester force-pushed the feature/uuidv7-monotonicity branch 7 times, most recently from cf9df36 to 8cd6bd7 Compare November 24, 2025 13:07
@ryanwinchester
Copy link
Contributor Author

ryanwinchester commented Nov 24, 2025

Another thing I just thought is that instead of using :persistent_term we could use the process dictionary to make them monotonic only per process, which could be used for batching.

updating uuidv7 options

Update Ecto.UUID.generate/1 docs

add tests for uuidv7 monotonicity

default UUIDv7 precision to nanosecond when monotonic
@ryanwinchester ryanwinchester force-pushed the feature/uuidv7-monotonicity branch from 8cd6bd7 to 3df02de Compare November 24, 2025 13:31
lib/ecto/uuid.ex Outdated

defp next_ascending(time_unit) when time_unit in [:millisecond, :nanosecond] do
timestamp_ref =
with nil <- :persistent_term.get({__MODULE__, time_unit}, nil) do
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The nil case should not happen, right?

Perhaps I would do: :persistent_term.get({__MODULE__, time_unit}, nil) || raise "Ecto has not been started" or something?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It shouldn't happen unless they were somehow using the Ecto.UUID module outside of an application or without starting Ecto.

The raise might be the safer option.

Copy link
Member

@josevalim josevalim left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I was travelling back. I have added two last (minor) comments.

@josevalim josevalim merged commit eb0a160 into elixir-ecto:master Nov 28, 2025
7 checks passed
@josevalim
Copy link
Member

💚 💙 💜 💛 ❤️

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants