Teknologi

UUIDv7 Bukan Jaminan Urut dan Tak Tertebak: Jebakan RFC 9562

UUIDv7 Bukan Jaminan Urut dan Tak Tertebak: Jebakan RFC 9562

UUIDv7 sering dipromosikan sebagai pengganti UUIDv4 untuk primary key karena katanya urut berdasarkan waktu, membawa timestamp pembuatan, dan tetap sulit ditebak. Tiga klaim itu tidak sepenuhnya benar. RFC 9562 yang terbit Mei 2024 mendefinisikan UUID sepanjang 128 bit, dan pada versi 7 hanya 48 bit pertama yang dipakai untuk timestamp milidetik, sisanya diisi version, variant, dan nilai acak atau counter. Robert Alexander dalam analisisnya pada 19 Oktober 2026 menunjukkan bahwa implementasi yang patuh RFC pun bisa melanggar sifat monotonik sekaligus sifat tak tertebak.

TL;DR

  • RFC 9562 hanya menjamin keunikan UUID, bukan urutan global atau ketaktertebakan.
  • Hanya 48 bit pertama UUIDv7 yang berisi timestamp milidetik, 74 bit sisanya bebas dipakai implementasi.
  • Dokumentasi PostgreSQL sendiri memperingatkan bahwa timestamp hasil ekstraksi belum tentu sama dengan waktu pembuatan UUID.
  • Generator yang memakai counter demi urutan justru menurunkan entropi dan membuat ID lebih mudah ditebak.
  • UUIDv7 tetap berguna untuk indeks yang ramah insert, asalkan urutan dan kerahasiaan tidak dijadikan asumsi keamanan.

Apakah UUIDv7 selalu berurutan?

Tidak. RFC 9562 tidak mewajibkan UUIDv7 terurut secara global, dan implementasi yang patuh standar tetap bisa menghasilkan urutan yang melompat saat ada beberapa proses generator berjalan bersamaan. Yang dijamin standar adalah keunikan, bukan monotonisitas.

Masalahnya muncul dari cara generator bekerja. Banyak library menambahkan counter kecil di bagian rand_a supaya UUID yang dibuat pada milidetik yang sama tetap naik. Selama satu proses dan satu jam sistem yang stabil, hasilnya terlihat rapi. Begitu ada dua pod Kubernetes yang masing-masing punya counter sendiri, urutan antar pod kembali acak. Insert tetap tersebar, dan klaim "selalu append-only" gugur.

Ada juga efek jam sistem. Timestamp UUIDv7 diambil dari jam mesin. Kalau NTP mengoreksi waktu mundur beberapa milidetik, UUID baru bisa bernilai lebih kecil dari UUID sebelumnya. Pada database yang mengandalkan urutan untuk pagination berbasis kursor, kejadian seperti ini cukup untuk membuat satu baris terlewat atau muncul dua kali. Itu bukan bug teori, karena koreksi waktu mundur adalah hal normal pada mesin virtual dan container yang di-suspend.

Yang perlu dicatat: sifat monotonik sebenarnya bisa ditegakkan, tapi harus oleh implementasi, bukan oleh format. RFC 9562 membuka ruang untuk metode khusus, misalnya counter, penggantian timestamp dengan nilai yang lebih besar dari sebelumnya, atau pengurutan ulang bit. Setiap metode punya konsekuensi sendiri, dan tidak ada satu metode yang diwajibkan. Artinya, dua layanan yang sama-sama menghasilkan UUIDv7 belum tentu bisa dibandingkan urutannya.

Kenapa timestamp di UUIDv7 tidak sama dengan waktu pembuatan?

Karena RFC mengizinkan implementasi mengubah isi field timestamp, dan dokumentasi PostgreSQL untuk fungsi uuid_extract_timestamp menyebut bahwa waktu yang diekstrak tidak selalu sama persis dengan waktu UUID dibuat. Timestamp di dalam ID adalah data yang bisa dimanipulasi, bukan catatan audit.

Ada beberapa alasan implementasi mengubah field itu. Pertama, untuk menjaga urutan saat jam mundur, generator bisa memakai timestamp terakhir yang pernah dikeluarkan, bukan jam saat ini. Kedua, untuk mengurangi tabrakan pada pembuatan massal, generator bisa membagi jatah waktu ke beberapa proses. Ketiga, untuk alasan privasi, sebagian generator sengaja menggeser atau mengosongkan sebagian field waktu.

Konsekuensi praktisnya sederhana: jangan hapus kolom created_at hanya karena UUID sudah membawa timestamp. Kalau aplikasi butuh waktu pembuatan yang bisa dipertanggungjawabkan, kolom terpisah dengan default now() tetap diperlukan. Menyimpan waktu di dalam primary key terlihat hemat satu kolom, tapi sumber kebenaran untuk audit bukan tempatnya di situ.

Hal yang sama berlaku saat melakukan backfill data lama. Baris yang dimigrasikan dengan UUIDv7 baru akan membawa timestamp migrasi, bukan timestamp asli. Kalau tim memakai kolom waktu di UUID untuk laporan, angka laporan itu akan melompat ke tanggal migrasi. Ini kesalahan yang mahal untuk diperbaiki setelah data masuk ke tabel produksi.

Apakah UUIDv7 masih aman dipakai sebagai ID publik?

Hanya jika ID itu tidak diperlakukan sebagai rahasia. UUIDv4 menyisakan 122 bit acak, sedangkan UUIDv7 hanya menyisakan 74 bit di luar timestamp, dan angka itu menyusut lagi ketika generator memakai sebagian bit untuk counter. ID yang muncul di URL publik tetap bisa dienumerasi kalau ruang acaknya kecil.

Perhatikan kombinasi yang paling berbahaya: timestamp presisi milidetik plus counter berurutan. Dari satu ID yang bocor, penyerang bisa menebak ID sebelum dan sesudahnya dalam rentang waktu yang sama, lalu menggeser jendela waktu ke belakang atau ke depan. Untuk objek yang sensitif, misalnya invoice, tiket dukungan, atau tautan berbagi dokumen, pola ini membuka jalur enumerasi yang tidak ada pada UUIDv4.

Solusinya bukan mengganti format, tapi memisahkan dua peran. Primary key boleh tetap UUIDv7 demi performa indeks, sementara ID yang tampil ke publik dibuat terpisah, acak penuh, dan tidak berkorelasi dengan waktu. Banyak tim mencampur keduanya, lalu bingung ketika halaman dengan ID berurutan bisa dipindai oleh bot pengumpul data.

UUIDv4 atau UUIDv7, mana yang sebaiknya dipakai?

Pilih berdasarkan beban tulis dan kebutuhan privasi, bukan berdasarkan tren. UUIDv7 menang pada tabel yang insert-nya tinggi karena nilainya cenderung naik sehingga halaman indeks lebih padat, sedangkan UUIDv4 lebih sederhana untuk kasus yang menuntut ketaktertebakan penuh.

PropertiUUIDv4UUIDv7
Bit acak122 bit74 bit di luar timestamp
Urutan berdasarkan waktuTidak adaAda, tapi tidak dijamin global
Timestamp pembuatanTidak ada48 bit milidetik, bisa dimodifikasi implementasi
Pola insert pada B-treeAcak, memicu page splitCenderung naik, lebih ramah cache
Risiko enumerasi dari ID publikRendahSedang sampai tinggi tergantung generator
Jaminan yang benar-benar dipegangKeunikanKeunikan

Untuk tabel log atau event dengan laju tulis tinggi, UUIDv7 biasanya pilihan yang lebih baik daripada UUIDv4 karena mengurangi fragmentasi indeks. Untuk tabel yang ID-nya tampil di URL dan tidak pernah dipakai untuk pengurutan, UUIDv4 masih pilihan yang lebih aman dan lebih mudah diprediksi perilakunya.

Ada jalan tengah yang sering dipakai: UUIDv7 untuk key internal, plus kolom terpisah untuk ID publik berbasis hash acak. Biaya tambahannya satu kolom dan satu indeks unik, jauh lebih murah daripada menambal kebocoran enumerasi setelah produksi berjalan.

Kapan UUIDv7 tetap pilihan tepat?

UUIDv7 tepat dipakai ketika keunikan lintas sistem jadi kebutuhan utama, insert terjadi sangat sering, dan ID tidak pernah diperlakukan sebagai rahasia. Contohnya tabel event, antrean pekerjaan, dan catatan audit internal yang hanya diakses lewat layanan dengan otorisasi.

Standar UUID sendiri sudah lama dipakai karena menyelesaikan masalah yang sulit: menghasilkan pengenal tanpa koordinasi pusat. Spesifikasi lengkapnya ada di RFC 9562, termasuk tabel layout bit per versi. Kalau implementasi memakai PostgreSQL, perilaku fungsi ekstraksi timestamp dijelaskan di dokumentasi fungsi UUID PostgreSQL, dan daftar lengkap anomali yang mungkin muncul dibahas di analisis Robert Alexander.

Yang perlu dihindari adalah menjadikan UUIDv7 sebagai dasar keputusan keamanan. Selama tim memperlakukan UUID sebagai pengenal, bukan token, sebagian besar jebakan di atas hilang. Sebaliknya, begitu UUID dipakai sebagai tautan berbagi yang "cukup sulit ditebak", asumsi keamanannya berpindah ke properti yang tidak dijamin standar.

Bagaimana cara menguji klaim UUIDv7 di aplikasi sendiri?

Uji dengan tiga skenario: dua generator paralel, jam yang mundur, dan pembuatan massal dalam satu milidetik. Ketiganya cukup untuk membuktikan apakah asumsi urutan yang dipakai aplikasi benar-benar berlaku di lingkungan produksi.

Skenario pertama, jalankan dua proses yang masing-masing membuat seribu UUID, lalu urutkan hasil gabungannya berdasarkan nilai UUID dan bandingkan dengan urutan waktu pembuatan yang dicatat terpisah. Kalau ada baris yang urutannya terbalik, berarti aplikasi tidak boleh mengandalkan UUID untuk pagination berbasis kursor. Skenario kedua, majukan jam sistem beberapa detik lalu kembalikan, dan lihat apakah generator menghasilkan nilai yang lebih kecil dari sebelumnya. Banyak library menangani ini dengan counter internal, tapi perilakunya berbeda antar implementasi dan antar versi library.

Skenario ketiga, buat sepuluh ribu UUID dalam satu milidetik dan hitung berapa banyak bit yang benar-benar acak. Caranya dengan melihat distribusi nilai pada byte terakhir. Kalau distribusinya sempit, kemungkinan besar generator memakai counter dan sisa entropinya jauh di bawah 74 bit. Hasil pengujian ini yang menentukan apakah ID tersebut layak muncul di URL publik.

Setelah tiga skenario itu, putuskan berdasarkan bukti, bukan dokumentasi library. Banyak pustaka UUIDv7 mengklaim "monotonic" padahal yang dimaksud hanya monotonic dalam satu proses. Perbedaan definisi itu tidak berbahaya selama aplikasi tidak bergantung padanya, tapi menjadi sumber bug yang sulit dilacak ketika tim lain menganggapnya jaminan global.

Satu hal lagi yang sering terlewat: pengujian harus dijalankan pada versi library yang dipakai produksi. Perilaku generator bisa berubah antar rilis minor, terutama pada library yang menambahkan optimasi counter. Kunci versi di lockfile, dan ulangi pengujian setiap kali versi naik.

FAQ

Apakah UUIDv7 lebih cepat dari UUIDv4 di PostgreSQL?

Untuk insert bervolume tinggi, sering ya, karena nilainya cenderung naik sehingga halaman indeks lebih padat dan page split berkurang. Selisihnya sangat bergantung pada pola tulis dan ukuran tabel, jadi ukur sendiri dengan data mirip produksi sebelum memutuskan migrasi.

Bisakah UUIDv7 dipakai sebagai kunci partisi berdasarkan waktu?

Bisa, tapi jangan mengandalkan field timestamp di dalam UUID sebagai satu-satunya sumber. Ekstrak nilainya, simpan di kolom terpisah, dan partisi berdasarkan kolom itu. Cara ini tetap benar meskipun implementasi generator mengubah atau menggeser timestamp.

Apakah UUIDv7 bentrok dengan UUIDv4 di tabel yang sama?

Tidak. Keduanya tetap UUID 128 bit dengan version dan variant berbeda, sehingga nilai yang dihasilkan tidak akan bertabrakan. Yang perlu diperhatikan justru campuran pola insert: baris v4 yang acak bisa memicu fragmentasi di antara baris v7 yang berurutan.

Apakah UUIDv7 boleh dipakai sebagai session token?

Tidak disarankan. Session token butuh entropi penuh dan tidak boleh punya komponen waktu yang bisa diprediksi. Pakai nilai acak khusus token, simpan hash-nya di database, dan biarkan UUID tetap berperan sebagai pengenal baris.

💬 Komentar (0)

Belum ada komentar. Jadilah yang pertama! 💬

Komentar akan muncul setelah moderasi.