<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Jules Smeets</title>
    <description>The latest articles on DEV Community by Jules Smeets (@juulsme).</description>
    <link>https://dev.to/juulsme</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3796399%2F102ca81b-0ce8-492a-b4c9-73147401feb8.jpeg</url>
      <title>DEV Community: Jules Smeets</title>
      <link>https://dev.to/juulsme</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/juulsme"/>
    <language>en</language>
    <item>
      <title>Dynamic machine ID leases in Elixir</title>
      <dc:creator>Jules Smeets</dc:creator>
      <pubDate>Wed, 19 Aug 2026 18:55:36 +0000</pubDate>
      <link>https://dev.to/juulsme/dynamic-machine-id-leases-in-elixir-4inj</link>
      <guid>https://dev.to/juulsme/dynamic-machine-id-leases-in-elixir-4inj</guid>
      <description>&lt;p&gt;Distributed ID generators often look simple: combine a timestamp, a counter, and a machine ID. &lt;a href="https://github.com/juulSme/NoNoncense" rel="noopener noreferrer"&gt;NoNoncense&lt;/a&gt; uses that idea for fast counter, sortable, and encrypted nonces. It is a wonderfully fast design, right up until two running nodes receive the same machine ID.&lt;/p&gt;

&lt;p&gt;The hard part is not incrementing a counter. It is deciding who may use each machine ID, especially when pods are replaced, nodes autoscale, deployments overlap, and network partitions happen. Two locally correct counters with the same machine ID can produce globally duplicate values.&lt;/p&gt;

&lt;p&gt;NoNoncense 1.x left that decision to application startup: derive an ID, initialize the nonce factory, and make the topology safe yourself. That is perfectly reasonable for a fixed fleet. Version 2.0 brings the decision into the supervision tree and gives different deployments a suitable way to establish exclusivity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where exclusivity comes from
&lt;/h2&gt;

&lt;p&gt;The strategy is not an afterthought; it expresses what makes an ID unique in a particular deployment:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SQL leases&lt;/strong&gt; coordinate through PostgreSQL or MySQL. They are a natural fit when the application already has a database and wants a durable, inspectable source of truth.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Valkey or Redis leases&lt;/strong&gt; use modern per-field hash TTLs for a compact, server-managed lease registry.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kubernetes StatefulSets&lt;/strong&gt; can use the pod ordinal through an environment variable when that ordinal is globally unique among nonce-generating pods.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Host identifiers&lt;/strong&gt; retain the fixed-node model for deployments with a known, stable node list.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The last two are intentionally uncomplicated: when the platform can prove an instance's identity, NoNoncense can use it directly. The interesting work begins when it cannot and the application needs to coordinate competing nodes.&lt;/p&gt;

&lt;h2&gt;
  
  
  One lifecycle, several coordinators
&lt;/h2&gt;

&lt;p&gt;For dynamic deployments, NoNoncense treats a machine ID as a lease. &lt;code&gt;NoNoncense.MachineId&lt;/code&gt; starts a &lt;code&gt;LeaseManager&lt;/code&gt; under supervision and waits for it to acquire an initial lease before application startup can complete. Only then are the configured nonce factories initialized. There is no interval in which a node generates values with an unproven ID.&lt;/p&gt;

&lt;p&gt;The manager then renews the lease in the background. Its strategy interface distinguishes a confirmed loss from an uncertain result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lease&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ttl_ms&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:lost&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:retry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;lost&lt;/code&gt; means the coordinator has positively rejected ownership, perhaps because the lease expired and somebody else claimed the ID. &lt;code&gt;retry&lt;/code&gt; means the result is uncertain: a timeout or a dropped connection might have happened before or after the coordinator applied the renewal. The manager retries uncertainty for a bounded period, but on confirmed loss or local expiry it erases the affected factories and calls the optional &lt;code&gt;:on_lease_lost&lt;/code&gt; callback. Continuing after that would trade an availability problem for a uniqueness problem, so the library chooses uniqueness.&lt;/p&gt;

&lt;p&gt;For dynamic strategies, it then retries acquisition with backoff and reinitializes the factories only after it holds a new lease. That makes recovery part of the component's normal lifecycle rather than a bespoke application failure path.&lt;/p&gt;

&lt;p&gt;The manager also has a small, in-memory lease cache. If the manager itself crashes and supervision restarts it, it first tries to renew the cached lease rather than needlessly claim another one of the 512 available IDs. The cache is deliberately not durable: after a node crash or forced kill, normal lease expiry remains the source of truth.&lt;/p&gt;

&lt;p&gt;This lifecycle is not limited to the included backends. A strategy is a small behaviour that implements &lt;code&gt;acquire/2&lt;/code&gt;, &lt;code&gt;renew/3&lt;/code&gt;, &lt;code&gt;release/2&lt;/code&gt;, and declares whether its IDs are deterministic. Applications can therefore bring a coordinator with their own allocation rules while keeping the same startup, recovery, and safety policy.&lt;/p&gt;

&lt;p&gt;With that shared lifecycle in place, the interesting question becomes how a coordinator makes ownership atomic without slowing the nonce path down.&lt;/p&gt;

&lt;h2&gt;
  
  
  SQL: let the database arbitrate the race
&lt;/h2&gt;

&lt;p&gt;The SQL strategy works with PostgreSQL and MySQL using a small, pre-populated lease table. Each possible machine ID has a row containing an expiry time, an ownership token, and a lock version.&lt;/p&gt;

&lt;p&gt;Acquisition is a two-stage optimistic operation:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Read a small set of expired candidate rows, ordered by ID.&lt;/li&gt;
&lt;li&gt;Attempt to claim one with an &lt;code&gt;UPDATE&lt;/code&gt; that matches both its ID and the &lt;code&gt;lock_version&lt;/code&gt; just read.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The update sets a new random token, moves &lt;code&gt;expires_at&lt;/code&gt; forward, and increments the version. If two nodes read the same expired row, they both try to update it, but only one update still matches the old version. The loser tries another candidate. The database turns a familiar optimistic-locking primitive into lease arbitration: no process-local lock, no table-wide lock, and no opportunity to overwrite a concurrent claim.&lt;/p&gt;

&lt;p&gt;The strategy uses &lt;code&gt;CURRENT_TIMESTAMP&lt;/code&gt; in the database for both expiry checks and new expirations. That is a small but important choice: eligibility is decided by one clock, rather than by application nodes whose clocks may disagree.&lt;/p&gt;

&lt;p&gt;Once claimed, the random token becomes the lease identity. Renewals and releases match both the machine ID and that token, so a node that has lost its lease cannot extend or release the next owner's row.&lt;/p&gt;

&lt;h2&gt;
  
  
  Valkey and Redis: turn field existence into ownership
&lt;/h2&gt;

&lt;p&gt;Redis 8 and Valkey 9 add &lt;code&gt;HSETEX&lt;/code&gt;: &lt;em&gt;conditional&lt;/em&gt; hash updates with a per-field TTL. That makes it possible to store all leases in one hash without a polling cleanup job.&lt;/p&gt;

&lt;p&gt;At first, an ID field appears sufficient:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"17" =&amp;gt; "random-token"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But &lt;code&gt;HSETEX&lt;/code&gt; conditions check field existence, not whether a field still holds a particular value. A renewal conditioned only on field &lt;code&gt;"17"&lt;/code&gt; could refresh an ID after another owner had taken it. That is exactly the operation a lease protocol must reject.&lt;/p&gt;

&lt;p&gt;NoNoncense writes two fields in one &lt;code&gt;HSETEX&lt;/code&gt; command, with the same TTL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"17"                    =&amp;gt; "random-token"
"token.17.random-token" =&amp;gt; "17"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first field makes occupied IDs easy to discover. The second is an ownership marker: its key contains both the ID and the token. On acquisition, &lt;code&gt;HSETEX ... FNX&lt;/code&gt; creates both fields only if neither already exists. On renewal or release, &lt;code&gt;HSETEX ... FXX&lt;/code&gt; succeeds only if both fields still exist.&lt;/p&gt;

&lt;p&gt;When a lease expires, both fields disappear together. A later owner will create a new token marker, not the old one. Therefore an old owner cannot refresh or delete the new owner's lease: its marker is absent, so the conditional command returns zero updated fields and the manager treats the lease as lost.&lt;/p&gt;

&lt;p&gt;This is the useful pattern here: when a datastore can conditionally test only existence, give each ownership generation its own expiring marker and update the resource and marker atomically. The marker converts an existence check into proof that this exact owner still has the right to act.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fast generation, deliberate ownership
&lt;/h2&gt;

&lt;p&gt;The new strategies make the same promise in different environments: nonce factories run only while the deployment can establish that their machine ID belongs to them. SQL gets there with a database clock, tokens, and optimistic compare-and-swap. Valkey and Redis get there with an atomic, expiring ownership marker. StatefulSets and fixed fleets can use the identity their platform already provides.&lt;/p&gt;

&lt;p&gt;The optional conflict guard adds another signal for connected Erlang nodes, and optional Telemetry reports acquisition, renewal, retries, loss, release, and conflicts. Neither feature touches the nonce generation hot path.&lt;/p&gt;

&lt;p&gt;The result is a deliberately asymmetric design: the path that generates a nonce stays tiny and fast, while the rare coordination path does the careful work needed to answer one question with confidence: who is allowed to generate values with this machine ID right now?&lt;/p&gt;

&lt;p&gt;NoNoncense 2.0 is available on &lt;a href="https://hex.pm/packages/no_noncense" rel="noopener noreferrer"&gt;Hex&lt;/a&gt;. The &lt;a href="https://no-noncense.hexdocs.pm/NoNoncense.MachineId.html" rel="noopener noreferrer"&gt;machine ID documentation&lt;/a&gt; and &lt;a href="https://no-noncense.hexdocs.pm/migration.html" rel="noopener noreferrer"&gt;migration guide&lt;/a&gt; cover configuration and upgrading from 1.x.&lt;/p&gt;

</description>
      <category>elixir</category>
      <category>distributedsystems</category>
    </item>
    <item>
      <title>JWTs in Elixir: fast parsing by plain pattern matching</title>
      <dc:creator>Jules Smeets</dc:creator>
      <pubDate>Thu, 26 Mar 2026 21:35:01 +0000</pubDate>
      <link>https://dev.to/juulsme/jwts-in-elixir-fast-parsing-by-plain-pattern-matching-52n5</link>
      <guid>https://dev.to/juulsme/jwts-in-elixir-fast-parsing-by-plain-pattern-matching-52n5</guid>
      <description>&lt;p&gt;JSON Web Tokens (JWTs) are the backbone of modern authentication. If you are building an API, chances are you are verifying a JWT on almost every single incoming request.&lt;/p&gt;

&lt;p&gt;Because it happens so frequently, JWT verification is a prime candidate for optimization. But if you look at how standard JWT verification works, there is a lot of hidden overhead.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Standard "Slow Path"
&lt;/h3&gt;

&lt;p&gt;A JWT consists of three parts separated by dots: &lt;code&gt;header.payload.signature&lt;/code&gt;. To verify a standard token, your application typically has to do the following:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Split the string by the &lt;code&gt;.&lt;/code&gt; character using &lt;code&gt;String.split/2&lt;/code&gt; and/or regex.&lt;/li&gt;
&lt;li&gt;Base64-decode the header.&lt;/li&gt;
&lt;li&gt;Parse the resulting JSON string into a map.&lt;/li&gt;
&lt;li&gt;Extract the &lt;code&gt;kid&lt;/code&gt; (Key ID) and &lt;code&gt;alg&lt;/code&gt; (Algorithm) claims.&lt;/li&gt;
&lt;li&gt;Look up the correct public or symmetric key.&lt;/li&gt;
&lt;li&gt;Verify the signature against the payload.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Steps 1 through 4 require allocating memory for new binaries, running a Base64 decoder, and firing up a JSON parser—all just to figure out &lt;em&gt;which key&lt;/em&gt; to use. &lt;/p&gt;

&lt;p&gt;If your application is minting its own tokens, 99% of the incoming requests are going to use your current, active signing key. Doing all that decoding and parsing on every request is wasted effort.&lt;/p&gt;

&lt;h3&gt;
  
  
  Our own header is always the same
&lt;/h3&gt;

&lt;p&gt;The realization that speeds this up is simple: &lt;strong&gt;if you control the active signing key, the JWT header is entirely predictable.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Instead of extracting the token's header to find the key, we can flip the process upside down. We can pre-calculate exactly what the Base64-encoded header will look like for our active key, and use it to fast-track both signing and verifying tokens.&lt;/p&gt;

&lt;h3&gt;
  
  
  Setup: Caching the Header
&lt;/h3&gt;

&lt;p&gt;The first step is to calculate the expected header at startup (or upon key rotation) and cache it. We use Erlang's &lt;code&gt;:persistent_term&lt;/code&gt; for blazing fast, concurrent read access.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;defmodule&lt;/span&gt; &lt;span class="no"&gt;MyApp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Jwt&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="nv"&gt;@doc&lt;/span&gt; &lt;span class="s2"&gt;"Run this at application startup or when rotating keys"&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;setup_fast_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;alg&lt;/span&gt; &lt;span class="p"&gt;\\&lt;/span&gt; &lt;span class="s2"&gt;"HS256"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;expected_header&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
      &lt;span class="c1"&gt;# we need deterministic JSON with predictable key ordering&lt;/span&gt;
      &lt;span class="sx"&gt;~s({"alg":"#{alg}","typ":"JWT","kid":"#{kid}"})&lt;/span&gt;
      &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Base&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url_encode64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;padding:&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="ss"&gt;:persistent_term&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:jwt_header&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected_header&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Fast-track verification
&lt;/h3&gt;

&lt;p&gt;The real magic happens when checking incoming requests. We use Elixir's binary pattern matching to instantly route the token based on whether it starts with our cached header:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;expected_header&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="ss"&gt;:persistent_term&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:jwt_header&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="c1"&gt;# Does the token start with our exact pre-calculated header?&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&amp;lt;^&lt;/span&gt;&lt;span class="n"&gt;expected_header&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;binary&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sx"&gt;?.&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload_and_sig&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;binary&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; 
      &lt;span class="n"&gt;fast_verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload_and_sig&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# ... slow path fallback ...&lt;/span&gt;
    &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
      &lt;span class="n"&gt;slow_verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The binary pattern matching is Elixir at its finest. The VM doesn't need to split strings or allocate new memory to do so. It simply looks at the bytes of the incoming token, checks if they match the pinned &lt;code&gt;expected_header&lt;/code&gt; variable followed by a &lt;code&gt;.&lt;/code&gt; character, and assigns the rest of the binary to &lt;code&gt;payload_and_sig&lt;/code&gt;. We now know the signing key and algorithm without parsing the header!&lt;/p&gt;

&lt;p&gt;We are not done, however, and can take pattern matching one step further to split the payload and signature. This relies on two things: we know the signature's length in advance and can use &lt;code&gt;byte_size/1&lt;/code&gt; to determine the length of the whole (in constant time!):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="c1"&gt;# For HS256, the encoded signature is always exactly 43 characters&lt;/span&gt;
&lt;span class="nv"&gt;@hs256_sig_len&lt;/span&gt; &lt;span class="mi"&gt;43&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;fast_verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload_and_sig&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;payload_len&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;byte_size&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload_and_sig&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nv"&gt;@hs256_sig_len&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;payload_and_sig&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;enc_payload&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;binary&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload_len&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sx"&gt;?.&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;enc_sig&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;binary&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;@hs256_sig_len&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
      &lt;span class="c1"&gt;# We skipped String.split/2 entirely!&lt;/span&gt;
      &lt;span class="c1"&gt;# Now we just decode the payload and verify the signature...&lt;/span&gt;
      &lt;span class="n"&gt;do_crypto_verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;enc_payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;enc_sig&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:malformed_token&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Signing
&lt;/h3&gt;

&lt;p&gt;Of course, we can also use our precomputed header to speed up token signing (also leveraging iolists to prevent creating unnecessary intermediate binaries):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;header&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="ss"&gt;:persistent_term&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:jwt_header&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;enc_payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;JSON&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;encode!&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Base&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url_encode64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;padding:&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;mac_base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sx"&gt;?.&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;enc_payload&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="n"&gt;sig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="ss"&gt;:crypto&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mac&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:hmac&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:sha256&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mac_base&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Base&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url_encode64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;padding:&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="no"&gt;IO&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;iodata_to_binary&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;mac_base&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sx"&gt;?.&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sig&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What if the header is not fully deterministic? Poly1305 and Base64 Magic
&lt;/h3&gt;

&lt;p&gt;The optimization above works flawlessly for algorithms like HS256 or EdDSA. But what if you are using a MAC generated via Poly1305? &lt;/p&gt;

&lt;p&gt;Poly1305 requires a unique &lt;code&gt;nonce&lt;/code&gt; for every single invocation to remain secure. This means our JWT header suddenly needs to look like this:&lt;br&gt;
&lt;code&gt;{"alg":"Poly1305","typ":"JWT","nonce":"&amp;lt;random_string&amp;gt;","kid":"my_key"}&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Because the &lt;code&gt;nonce&lt;/code&gt; changes every time, we can no longer pre-calculate the &lt;em&gt;entire&lt;/em&gt; Base64 encoded header. Does this mean we have to fall back to the slow path? Not if we use a little Base64 math.&lt;/p&gt;

&lt;p&gt;Base64 works by encoding 3 bytes of raw data into 4 characters. If a string's byte length is exactly a multiple of 3, it encodes cleanly without padding (&lt;code&gt;=&lt;/code&gt;) and without carrying over bits into the next character block. Such encoded strings can then be safely concatenated without invalidating the encoding!&lt;/p&gt;

&lt;p&gt;We can exploit this by carefully crafting our JSON header so the static parts are multiples of 3 bytes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Part 1: &lt;code&gt;{"alg":"Poly1305","typ":"JWT","nonce":"&lt;/code&gt; (Length is 39, and 39 / 3 = 13. Perfect!)&lt;/li&gt;
&lt;li&gt;Part 2: &lt;code&gt;&amp;lt;dynamic_nonce&amp;gt;",&lt;/code&gt; (The nonce is 12 bytes, which encodes to 16 bytes. We add &lt;code&gt;",&lt;/code&gt; and part 2 is now 18 bytes - dividable by 3!)&lt;/li&gt;
&lt;li&gt;Part 3: &lt;code&gt;"kid":&lt;/code&gt; (Length is 6!)&lt;/li&gt;
&lt;li&gt;Part 4: &lt;code&gt;"my_key"}&lt;/code&gt; (The last part has no length requirement because nothing will be concatenated to its tail)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because Parts 1, 2 and 3 are multiples of 3, we can safely pre-calculate their Base64 equivalents and concatenate them with the encoded dynamic bits later:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Pre-encoded static chunks (safe to concatenate)&lt;/span&gt;
&lt;span class="nv"&gt;@p1305_h&lt;/span&gt; &lt;span class="sx"&gt;~s({"alg":"Poly1305","typ":"JWT","nonce":")&lt;/span&gt; &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Base&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url_encode64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;padding:&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;@p1305_kid_seg&lt;/span&gt; &lt;span class="sx"&gt;~s("kid":)&lt;/span&gt; &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Base&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url_encode64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;padding:&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;setup_poly1305_fast_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;exp_htail&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sx"&gt;~s("#{kid}"})&lt;/span&gt; &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Base&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url_encode64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;padding:&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="ss"&gt;:persistent_term&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:jwt_p1305_header&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exp_htail&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, we can add a new pattern match to our &lt;code&gt;verify/1&lt;/code&gt; function to catch Poly1305 tokens. We just extract the encoded &lt;code&gt;nonce_seg&lt;/code&gt; right out of the middle of the binary string:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;exp_header&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="ss"&gt;:persistent_term&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:jwt_header&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;exp_htail&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="ss"&gt;:persistent_term&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:jwt_p1305_header&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="c1"&gt;# 1. The Simple Fast Path (HS256, etc)&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&amp;lt;^&lt;/span&gt;&lt;span class="n"&gt;exp_header&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;binary&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sx"&gt;?.&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload_and_sig&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;binary&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; 
      &lt;span class="n"&gt;fast_verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload_and_sig&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# 2. The Base64 Magic Fast Path (Poly1305)&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&gt;@p1305_h&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nonce_seg&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;binary&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;@p1305_kid_seg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;^&lt;/span&gt;&lt;span class="n"&gt;exp_htail&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;binary&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sx"&gt;?.&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload_and_sig&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;binary&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
      &lt;span class="n"&gt;fast_p1305_verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nonce_seg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload_and_sig&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# 3. THE SLOW PATH&lt;/span&gt;
    &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; 
      &lt;span class="n"&gt;slow_verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By ensuring our JSON keys and values add up to multiples of 3, we can continue to use Elixir's blazing fast binary pattern matching to route tokens, skipping the JSON parser entirely even when parts of the header are highly dynamic.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Fallback
&lt;/h3&gt;

&lt;p&gt;Of course, the slow path cannot be completely removed. If you rotate your signing keys, users with valid tokens signed by the &lt;em&gt;old&lt;/em&gt; key will still need to access your API. &lt;/p&gt;

&lt;p&gt;Because their tokens will have a different &lt;code&gt;kid&lt;/code&gt; in the header, the fast path pattern match will naturally fail. The &lt;code&gt;case&lt;/code&gt; statement falls back to &lt;code&gt;slow_verify&lt;/code&gt;, which does the traditional Base64 decoding, JSON parsing, and dynamic key lookup. &lt;/p&gt;

&lt;p&gt;The beauty of this architecture is that the fast path handles 99% of your traffic with near-zero overhead, while the slow path gracefully catches edge cases, legacy tokens, and key rotations without the caller ever needing to know the difference. And although we don't parse the headers, we don't assume anything here; because of the binary pattern matching, there's not a single header byte that is not exactly what we expect it to be. JWT-verification speed is improved by about 30% for normal-sized JWTs.&lt;/p&gt;

&lt;p&gt;A real-world example (using only OTP!) that supports key rotation and multiple signing algorithms including HMAC-SHA256, EdDSA and Poly1305 can be found in Charon's &lt;a href="https://github.com/weareyipyip/charon/blob/main/lib/charon/token_factory/jwt.ex" rel="noopener noreferrer"&gt;JWT token factory&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>elixir</category>
      <category>jwt</category>
    </item>
    <item>
      <title>Why We Stopped Using UUIDv4 in Elixir (And What We Built Instead)</title>
      <dc:creator>Jules Smeets</dc:creator>
      <pubDate>Fri, 27 Feb 2026 18:14:21 +0000</pubDate>
      <link>https://dev.to/juulsme/why-we-stopped-using-uuidv4-in-elixir-and-what-we-built-instead-59i3</link>
      <guid>https://dev.to/juulsme/why-we-stopped-using-uuidv4-in-elixir-and-what-we-built-instead-59i3</guid>
      <description>&lt;p&gt;If you’re building a new Phoenix application today, deciding on a primary key strategy usually takes about three seconds. You don't want to use standard auto-incrementing integers because they leak your business volume (e.g., "Oh, I'm only user #104? This app is a ghost town").&lt;/p&gt;

&lt;p&gt;So, you do what everyone does: you reach for &lt;code&gt;Ecto.UUID&lt;/code&gt;. It's built-in, it's unpredictable, and it solves the problem.&lt;/p&gt;

&lt;p&gt;But as your application scales, UUIDv4 becomes a silent performance killer. After wrestling with database bloat and terrible API developer experience, we decided to build a better way. Enter &lt;strong&gt;&lt;a href="https://hexdocs.pm/once" rel="noopener noreferrer"&gt;Once&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Here is why we ditched UUIDs, and how you can get Stripe-style, high-performance IDs in your Elixir app in about two minutes.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Problem with UUIDv4: Database Fragmentation
&lt;/h3&gt;

&lt;p&gt;PostgreSQL (like most relational databases) uses B-trees for its indexes. B-trees are incredibly fast, but they have a distinct preference: they absolutely &lt;em&gt;love&lt;/em&gt; sequential data.&lt;/p&gt;

&lt;p&gt;When you insert a sequential ID (like a standard integer or a time-sorted Snowflake ID), Postgres cleanly appends it to the right edge of the index.&lt;/p&gt;

&lt;p&gt;UUIDv4, however, is purely random. When you insert a million random UUIDs, Postgres has to constantly search for the correct leaf node to insert the new value. This causes massive "page splits," fragments the index, and destroys your cache locality.&lt;/p&gt;

&lt;p&gt;Over time, your index size balloons, your write I/O skyrockets, and your database spends precious memory caching index pages that are mostly empty.&lt;/p&gt;

&lt;h3&gt;
  
  
  The API UX Problem: "Type Confusion"
&lt;/h3&gt;

&lt;p&gt;Beyond database performance, UUIDs are completely hostile to human developers.&lt;/p&gt;

&lt;p&gt;If you are looking at a log file or an API payload and see &lt;code&gt;9f1c0d3a-2b7e-49a1-8d5c-9123456789ab&lt;/code&gt;, what is it? Is it a User ID? A Payment ID? A Subscription ID? If a frontend developer accidentally passes an Organization ID into a User endpoint, the system will just return a generic &lt;code&gt;404 Not Found&lt;/code&gt;, leading to hours of frustrating debugging.&lt;/p&gt;

&lt;p&gt;Stripe solved this years ago by prefixing their IDs: &lt;code&gt;cus_...&lt;/code&gt; for customers, &lt;code&gt;ch_...&lt;/code&gt; for charges. It is self-documenting, type-safe, and universally loved by developers. We wanted that exact experience in Ecto.&lt;/p&gt;

&lt;h3&gt;
  
  
  Enter &lt;code&gt;Once&lt;/code&gt;: The Best of Both Worlds
&lt;/h3&gt;

&lt;p&gt;We built &lt;a href="https://github.com/juulSme/Once" rel="noopener noreferrer"&gt;Once&lt;/a&gt; as a custom Ecto type to solve both the database performance problem and the API usability problem, without compromising on security.&lt;/p&gt;

&lt;p&gt;Under the hood, &lt;code&gt;Once&lt;/code&gt; is powered by &lt;a href="https://github.com/juulSme/NoNoncense" rel="noopener noreferrer"&gt;NoNoncense&lt;/a&gt;, a distributed, lock-free ID generator that uses Erlang's &lt;code&gt;:atomics&lt;/code&gt; to generate tens of millions of 64-bit IDs per second. Because it's 64-bit (instead of UUID's 128-bit), it immediately cuts your ID storage footprint in half.&lt;/p&gt;

&lt;h4&gt;
  
  
  The "Masking" Magic
&lt;/h4&gt;

&lt;p&gt;You usually have to make a painful choice:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Use sequential integers (Fast database, but leaks business metrics).&lt;/li&gt;
&lt;li&gt;Use random strings (Protects metrics, but slow database).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;With &lt;code&gt;Once&lt;/code&gt; masking, you don't have to choose. You configure your Ecto schema to generate a sequential, counter-based ID. When Ecto saves the record to your database, it stores a raw, 64-bit integer. Your database is perfectly happy, and your B-tree indexes stay perfectly compact.&lt;/p&gt;

&lt;p&gt;But when Ecto &lt;em&gt;loads&lt;/em&gt; that record back into your Elixir application, &lt;code&gt;Once&lt;/code&gt; uses blazing-fast encryption to transparently mask the integer, and prepends your chosen prefix.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;As a standard bigint in your database:&lt;/strong&gt; &lt;code&gt;98770186085072901&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;In your Elixir App / JSON API:&lt;/strong&gt; &lt;code&gt;"usr_4Tnk9-GqJ-s"&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your database gets perfectly optimized sequential writes. Your API consumers get unpredictable, Stripe-style, self-documenting strings.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fully Composable: Pick What You Need
&lt;/h3&gt;

&lt;p&gt;The best part about &lt;code&gt;Once&lt;/code&gt; is that &lt;strong&gt;these features are fully composable and optional.&lt;/strong&gt; You can tailor the ID generation to your exact needs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Just want unique IDs?&lt;/strong&gt; Use the defaults. You get 64-bit integers that are incredibly fast to generate and index in your database, and url64-encoded strings like &lt;code&gt;"AV7m9gAAAAU"&lt;/code&gt; in Elixir.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Need chronological sorting?&lt;/strong&gt; Change to &lt;code&gt;nonce_type: :sortable&lt;/code&gt;. You get a Snowflake-like ID where the first 42 bits are a timestamp, meaning &lt;code&gt;ORDER BY id&lt;/code&gt; works perfectly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Want masked IDs?&lt;/strong&gt; Just add &lt;code&gt;mask: true&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Need true unpredictability at rest?&lt;/strong&gt; If you don't want to use masking and genuinely need UUIDv4-style randomness stored in your database, switch to &lt;code&gt;nonce_type: :encrypted&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Want Prefixes?&lt;/strong&gt; Just add &lt;code&gt;prefix: "req_"&lt;/code&gt; to any of the above.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Want to persist the prefix?&lt;/strong&gt; Use a binary database format like &lt;code&gt;db_format: :hex&lt;/code&gt; and add &lt;code&gt;persist_prefix: true&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Want a different API format like integers?&lt;/strong&gt; Use &lt;code&gt;ex_format: :unsigned&lt;/code&gt; to render IDs like &lt;code&gt;"usr_98770186085072901"&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can combine all of these as you see fit, and automagically generate distributed IDs with just the features that you need and nothing more.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why not UUIDv7?
&lt;/h3&gt;

&lt;p&gt;While UUIDv7 solves the index fragmentation problem, it's not as flexible as &lt;code&gt;Once&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Storage:&lt;/strong&gt; UUIDv7 is still a bloated 128-bit identifier. Once cuts that in half (64-bit).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API UX:&lt;/strong&gt; UUIDv7 still looks like a massive, unreadable string (018e4b...). Once gives you Stripe-style usr_... prefixes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Business Leakage/Security:&lt;/strong&gt; UUIDv7 visibly leaks the exact millisecond a record was created. Once uses masking to hide the underlying counter/timestamp entirely.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The 2-Minute Phoenix Quickstart
&lt;/h3&gt;

&lt;p&gt;Here is how easy it is to drop into a Phoenix project.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Add the dependencies to &lt;code&gt;mix.exs&lt;/code&gt;:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;deps&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:once&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"~&amp;gt; 1.2"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Initialize the generator in your &lt;code&gt;application.ex&lt;/code&gt;:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Create a unique Machine ID for this node (supports up to 512 nodes in a cluster)&lt;/span&gt;
&lt;span class="n"&gt;machine_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;NoNoncense&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;MachineId&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;node_list:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="no"&gt;Node&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;()])&lt;/span&gt;

&lt;span class="c1"&gt;# Initialize a NoNoncense instance for Once with a secret key for masking&lt;/span&gt;
&lt;span class="c1"&gt;# (Pull this from your environment in production!)&lt;/span&gt;
&lt;span class="n"&gt;secret_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"ONCE_SECRET_KEY"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"my supersecret dev key"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="no"&gt;NoNoncense&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;name:&lt;/span&gt; &lt;span class="no"&gt;Once&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;machine_id:&lt;/span&gt; &lt;span class="n"&gt;machine_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;base_key:&lt;/span&gt; &lt;span class="n"&gt;secret_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that &lt;code&gt;NoNoncense&lt;/code&gt; is not a GenServer; it keeps its write-once-read-often state in &lt;code&gt;:persistent_term&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Define your Ecto Schema:&lt;/strong&gt;&lt;br&gt;
Just set &lt;code&gt;Once&lt;/code&gt; as your primary key, give it a prefix, and turn on masking.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;defmodule&lt;/span&gt; &lt;span class="no"&gt;MyApp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Accounts&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;User&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="no"&gt;Ecto&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Schema&lt;/span&gt;

  &lt;span class="nv"&gt;@primary_key&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;Once&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;autogenerate:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;prefix:&lt;/span&gt; &lt;span class="s2"&gt;"usr_"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;mask:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="n"&gt;schema&lt;/span&gt; &lt;span class="s2"&gt;"users"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;field&lt;/span&gt; &lt;span class="ss"&gt;:email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:string&lt;/span&gt;
    &lt;span class="n"&gt;timestamps&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That’s it. You now have locally unique (within your app or domain), Stripe-prefixed, cryptographically masked 64-bit IDs that won't destroy your database indexes.&lt;/p&gt;

&lt;p&gt;If you’re tired of the UUID performance tax or just want a better developer experience for your API consumers, give &lt;code&gt;Once&lt;/code&gt; a try on your next project.&lt;/p&gt;

&lt;p&gt;Check out the full documentation and advanced configuration options on &lt;a href="https://hexdocs.pm/once" rel="noopener noreferrer"&gt;HexDocs for Once&lt;/a&gt; and &lt;a href="https://hexdocs.pm/no_noncense" rel="noopener noreferrer"&gt;NoNoncense&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>elixir</category>
      <category>database</category>
      <category>distributedsystems</category>
    </item>
  </channel>
</rss>
