{"post":{"slug":"uuid-vs-bigint-keys-2026","title":"UUID vs Bigint Keys: The Tradeoff Nobody Measures","excerpt":"Sequential integers are compact and reveal your row counts. UUIDs are opaque and generatable anywhere, and they cost more in storage, index size, and insert performance.","category":"code","date":"February 3, 2026","author":"ByteLedger Team","content":"<span class=\"dropcap\">T</span>he primary key type is one of the earliest schema decisions and one of the hardest to change later. It affects storage, index size, insert performance, and what your identifiers reveal to anyone who sees them.\n\nThe argument is usually framed as opacity versus performance. That is roughly right and misses which performance, and by how much.\n\n## What changed in 2026\n\n- **Time-ordered UUIDs became the recommended default** where UUIDs are needed, largely removing the insert-fragmentation objection — see [UUID v7](/blog/uuid-v7-explained-2026).\n- **Distributed generation became more common.** More systems needed identifiers created outside a single database.\n- **Enumeration attacks stayed relevant.** Sequential identifiers in public URLs continued to be a routine finding in security reviews.\n- **The hybrid pattern spread.** Internal integer keys with external opaque identifiers became a widely-used compromise.\n\n## The costs, concretely\n\n| | Bigint | Random UUID | Time-ordered UUID |\n| --- | --- | --- | --- |\n| Storage per key | 8 bytes | 16 bytes | 16 bytes |\n| Insert position | Sequential | Random | Sequential |\n| Page splits | None | Frequent | None |\n| Index size | Smallest | Larger | Larger |\n| Generatable offline | No | Yes | Yes |\n| Reveals row count | Yes | No | No |\n| Reveals creation time | No | No | Yes, roughly |\n\nThe storage difference is twice per key, and it appears everywhere: the primary key, every foreign key referencing it, and every index containing it. In a clustered table, secondary indexes carry the primary key too, so the multiplier is larger than it first appears — see [clustered indexes](/blog/clustered-indexes-explained-2026).\n\nThe insert behaviour is the larger effect. Random UUIDs insert at arbitrary positions, splitting pages and fragmenting storage. On a write-heavy table that is a substantial and ongoing cost, not a one-off.\n\n## What sequential keys leak\n\nThe case against integers is not aesthetic.\n\n**Row counts and growth.** An identifier of 4,817 in a URL tells anyone your system has around 4,817 of that thing. Comparing two over time gives a growth rate. For a business that is competitive information given away in a URL.\n\n**Enumerability.** Sequential identifiers let anyone iterate. If authorisation is anywhere less than perfect, that turns a single leaked record into a systematic extraction. Sequential identifiers do not cause the vulnerability; they make exploiting it trivial.\n\n**Cross-referencing.** Identifiers that increase together suggest records created together, which can reveal relationships you did not intend to expose.\n\nNone of this matters for a table nobody outside your system ever sees. All of it matters for anything in a URL, an API response, or a document sent to a customer.\n\n## The hybrid that resolves it\n\nThe pattern most teams land on: **a sequential integer as the internal primary key, and a separate opaque identifier for external use.**\n\nJoins, foreign keys, and indexes all use the compact integer, so you keep the storage and insert-performance benefits internally. Anything leaving your system uses the opaque identifier, indexed separately.\n\nThe cost is one extra column and one extra unique index. In exchange you get compact internal keys and no information leakage externally, which is strictly better than either pure approach for anything with an external surface.\n\nWhere you genuinely need identifiers generated outside the database — multiple services creating records independently, offline clients, or merging data from several sources — a time-ordered UUID as the primary key is the right choice, and it removes the fragmentation problem that made random UUIDs painful.\n\n## Common mistakes\n\n- **Random UUIDs as a clustered primary key.** Fragmentation with no benefit over time-ordered.\n- **Sequential integers in public URLs.** Leaks counts and enables enumeration.\n- **UUIDs on internal-only tables.** Overhead for nothing.\n- **Storing UUIDs as text.** Roughly double the size again; use the native type.\n- **Assuming UUID uniqueness removes the need for a unique constraint.** Bugs generate duplicates.\n- **Changing key type later.** Extremely expensive on a large table with foreign keys.\n\n## FAQ\n\n**Which should I default to?**\nSequential integers for internal keys, with a separate opaque identifier where records are externally visible. Time-ordered UUIDs where identifiers must be generated outside the database.\n\n**How much does the storage difference really matter?**\nOn small tables, negligible. On large tables with several indexes and many foreign key references, it compounds into a real fraction of total storage and cache — measure rather than assume.\n\n**Do time-ordered UUIDs leak anything?**\nThe approximate creation time, by design. Usually acceptable and worth knowing if creation timing is sensitive.\n\n**Can I migrate later?**\nTechnically yes, and it means rewriting the table and every referencing foreign key. Treat it as a design-time decision.\n\n## Where to go next\n\nFor the time-ordered variant, read [UUID v7](/blog/uuid-v7-explained-2026). For why insert order matters so much, [clustered indexes](/blog/clustered-indexes-explained-2026), and for what happens when integer keys run out, [sequence exhaustion](/blog/sequence-exhaustion-2026).","hue":313,"metaTitle":"UUID vs Bigint Keys: The Tradeoff Nobody Measures","metaDescription":"Comparing UUID and integer primary keys on storage, index size, insert performance, and information leakage, and where time-ordered UUIDs fit.","keywords":["uuid vs bigint","primary key choice","sequential id leakage","index size overhead","distributed id generation","insert fragmentation","database key design","schema design 2026"],"tldr":["**UUIDs are twice the size and appear in every index** — the overhead multiplies across a table's indexes.","**Random UUIDs fragment clustered storage** — inserts land at arbitrary positions and split pages.","**Sequential integers leak information** — row counts, growth rate, and enumerable identifiers.","**Time-ordered UUIDs get most of both** — opacity with sequential insert behaviour.","**Skip:** UUIDs on internal tables nobody outside your system references. The overhead buys nothing there."],"readTime":"4 min read","wordCount":817},"related":[{"slug":"uuid-v7-explained-2026","title":"UUID v7: Opaque Identifiers That Insert Sequentially","excerpt":"Random UUIDs fragment your storage. Version 7 puts a timestamp in the high bits, so identifiers sort roughly by creation time and insert cleanly at the end.","category":"code","date":"January 1, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":304},{"slug":"clustered-indexes-explained-2026","title":"Clustered Indexes: When Physical Order Matters","excerpt":"Storing rows in index order makes range scans dramatically faster and makes the choice of key a decision you live with, because inserting in the wrong order fragments everything.","category":"code","date":"January 8, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":317},{"slug":"partial-indexes-explained-2026","title":"Partial Indexes: Indexing Only the Rows That Matter","excerpt":"Most queries against a large table only ever touch a small slice of it. A partial index covers just that slice, and is a fraction of the size and cost.","category":"code","date":"August 12, 2026","author":"ByteLedger Team","readTime":"5 min read","hue":316}],"sameCategory":[{"slug":"bazel-build-explained-2026","title":"Bazel Explained: What Hermetic Builds Actually Cost","excerpt":"Bazel delivers reproducible, precisely cached builds and demands that you describe everything. Here is what you get, what it costs, and who should say no.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":145},{"slug":"graphql-federation-2026","title":"GraphQL Federation: One Graph, Many Teams","excerpt":"Federation composes independently-owned subgraphs into a single API. Here is what it solves, the operational cost it adds, and when a simpler gateway is enough.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":300},{"slug":"hermetic-builds-explained-2026","title":"Hermetic Builds Explained: Same Inputs, Same Output","excerpt":"A hermetic build depends only on declared inputs. Here is what that property gives you, the common leaks that break it, and how to move toward it incrementally.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":130},{"slug":"monorepo-tooling-compared-2026","title":"Monorepo Tooling Compared: What You Actually Need","excerpt":"Monorepo tools solve task orchestration and caching, not repository layout. Here is what each tier provides and how to tell when you have outgrown the simple option.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":150}],"newer":{"slug":"umbrella-insurance-guide-2026","title":"Umbrella insurance guide 2026: when you actually need it and the cost ranges","excerpt":"Umbrella insurance in 2026 — who genuinely needs it, the cost ranges for $1M to $5M coverage, and the underlying coverage prerequisites.","category":"finance","date":"February 3, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":310},"older":{"slug":"vector-database-comparison-2026","title":"Vector database comparison 2026: Pinecone, Weaviate, Qdrant, pgvector, Milvus","excerpt":"A broader survey of vector databases in 2026 — five serious options compared on performance, pricing, and the deployment shape that should decide for you.","category":"ai","date":"February 3, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":184}}