DEV Community

Cover image for Rate Limiting Access Codes: The Delicate Balance Between Security and UX
Howard Shaw
Howard Shaw

Posted on

Rate Limiting Access Codes: The Delicate Balance Between Security and UX

Rate Limiting Access Codes: The Delicate Balance Between Security and UX

When building DocBeacon, we faced a common but tricky engineering challenge: how to protect resources secured by short access codes without frustrating legitimate users.

The Strategic Choice: When to Increment?

The core of rate limiting lies in deciding which events trigger the counter.

  1. Strict Enforcement (Count Every Request): Every attempt, valid or not, consumes the limit. While secure, this often leads to "false positives"—locking out users due to flaky connections, page refreshes, or shared IP environments.
  2. Adaptive Enforcement (Count Only Failures): We opted for this approach. By only incrementing the counter on incorrect guesses, we ensure that a user with the correct code is never blocked by the security layer.

The Security Math

Is "counting only failures" risky? Let's look at the entropy. For a code of length L using a character set of size S, the total combinations C is:

C = S^L

If we allow n attempts per window, the probability P of a successful brute-force attack is:

P = n / C

By choosing an appropriate L and S, even with a generous n, the risk remains statistically negligible (often < 0.001%), making the UX gain far outweigh the marginal security risk.

Implementation Pattern: The Verification Flow

Instead of sharing our production code, here is the high-level logic pattern. Note how the rate limit is checked and incremented only after a failed validation:

// A generalized pattern for secure verification
async function verifyAccessWithLimit(inputCode, context) {
  const { shareId, clientIp } = context;
  const identifier = `limit:${shareId}:${clientIp}`;

  // 1. Check if the user is already blocked
  const isBlocked = await rateLimiter.isLimitReached(identifier);
  if (isBlocked) {
    throw new Error("Too many attempts. Please try again later.");
  }

  // 2. Validate the code
  const isValid = await validateCode(inputCode);

  if (isValid) {
    return { success: true }; // Valid users are never penalized
  }

  // 3. Increment counter ONLY on failure
  await rateLimiter.recordFailure(identifier);
  return { success: false, error: "Invalid code" };
}
Enter fullscreen mode Exit fullscreen mode

Key Design: Composite Identifiers

To prevent one attacker from affecting the entire system, we use composite keys. This ensures that rate limits are isolated:

// Abstracting the key generation to ensure isolation
function generateRateLimitKey(resourceId, userIp) {
  // Combining resource and IP ensures that an attack on 
  // Resource A doesn't block legitimate access to Resource B.
  return `ratelimit:res_${resourceId}:ip_${userIp}`;
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

True security is about proportional response. By focusing our defenses on failure patterns rather than total traffic, we created a system for DocBeacon that feels invisible to the user but remains an impenetrable wall for brute-force scripts.

Top comments (0)