Using database primary keys directly in public APIs creates security and scalability risks. While UUIDs solve the enumeration problem, they introduce their own trade-offs. Sqids offer a middle ground, but understanding their limitations is critical for making the right architectural choice.
Exposing sequential integer IDs in your API endpoints (like /users/1 or /orders/42) is a classic anti-pattern. It makes resource enumeration trivial for attackers, leaks business intelligence (e.g., how many users you have), and creates a tight coupling between your public API and your database schema. Changing your primary key strategy later becomes a breaking change for all clients.
The UUID Solution and Its Costs
Universally Unique Identifiers (UUIDs) are the standard answer to this problem. They provide global uniqueness, prevent enumeration, and decouple the public ID from the database structure. However, they come with significant trade-offs:
- URL ugliness:
https://api.example.com/users/550e8400-e29b-41d4-a716-446655440000is unwieldy and hard to communicate. - Storage overhead: UUIDs (128-bit) are larger than 64-bit integers, impacting index size and memory usage.
- Indexing performance: UUIDs, especially v4 (random), can lead to index fragmentation and slower insert performance in some databases compared to sequential integers.
- Stateful generation: For distributed systems, generating UUIDs requires coordination (e.g., via a central service or using v1 with MAC addresses) to avoid collisions, adding complexity.
Sqids: A Stateful Obfuscation Layer
Sqids (and its predecessor, Hashids) offer a different approach. They are not random identifiers; they are deterministic encodings of your internal integer IDs. The core principle is simple:
- Internal State: Your database continues to use fast, sequential
BIGINTprimary keys. - On-the-Fly Encoding: When sending data to a client, you encode the internal ID into a short, URL-friendly string (e.g.,
1->"u9"). - On-the-Fly Decoding: When a client sends a request with a public ID, you decode it back to the internal integer to perform database lookups.
This approach provides several benefits:
- Obfuscation: The public ID reveals nothing about the order or scale of your data.
- Compact URLs: Short, readable strings that are easy to share and type.
- Stateless & Fast: No database lookups for ID generation; encoding/decoding is a CPU-bound operation that's typically very fast.
- Database Performance: You retain the benefits of integer primary keys for indexing and joins.
The Critical Trade-offs and Limitations
This is not a silver bullet. The choice between Sqids and UUIDs hinges on your system's architecture and requirements.
1. Security Through Obscurity (With a Caveat)
Sqids are not cryptographically secure. They are obfuscation, not encryption. If your salt (the secret key used in encoding) is compromised, an attacker can reverse-engineer the encoding scheme and generate valid IDs for any internal ID. The salt must be kept secret and treated like any other credential. For truly public, untrusted environments, this risk may be unacceptable.
2. Distributed System Coordination
This is the most significant limitation. Sqids are deterministic and require the same salt across all instances. In a distributed system with multiple API nodes, you must ensure all nodes share the same secret salt. If you scale horizontally without a shared secret, you'll get inconsistent encoding, breaking client requests. UUIDs, by contrast, are inherently globally unique and require no coordination.
3. No Built-in Uniqueness Guarantee
Sqids encode integers. If you have two different internal IDs, you will get two different Sqids. However, the encoding is not guaranteed to be unique across all possible integer ranges (though collisions are statistically improbable for typical use). The uniqueness is ultimately guaranteed by your primary key.
4. Migration Complexity
If you need to migrate an existing system from integer IDs to Sqids, you must handle both formats during a transition period, adding complexity to your API layer.
Architectural Pattern: A Hybrid Approach
A pragmatic pattern emerges:
- Use internal
BIGINTorSERIALprimary keys for your database tables. This ensures optimal performance for queries, joins, and sorting. - Generate a public-facing, obfuscated ID (Sqids, or a similar encoding) as a separate, indexed column in your database. This column is used for all external API references.
- For distributed systems, consider using a UUIDv4 as the public-facing ID. While it has the downsides mentioned, it provides guaranteed global uniqueness without coordination. You can still use a
BIGINTinternally for performance.
Implementation Workflow
The operational flow is straightforward:
- API Response: When serializing a
Userobject withid: 1, transform it topublic_id: "u9"using your Sqids encoder. - API Request: When receiving a request to
GET /users/u9, decode"u9"back to1. - Database Query: Use the decoded integer
1to fetch the record from youruserstable.
This keeps your database layer clean and performant while providing a safe, user-friendly public interface.
Conclusion: Context is King
Sqids are an excellent tool for single-node applications, microservices with shared secrets, or coordinated systems where you want to improve API usability and security without sacrificing database performance. They are a form of controlled obfuscation.
However, for globally distributed, untrusted environments where you cannot guarantee a shared secret across all nodes, or where cryptographic security is a hard requirement, UUIDs remain the safer, more robust choice despite their formatting and performance costs.
The decision isn't about which is universally "better," but which trade-offs align with your specific system's constraints: the scale of your data, your distribution model, and your security posture.




Comments
Please log in or register to join the discussion