UnlockOS Developers
← Back to blog
🔐

Resilient Authentication: Implementing Robust Token Retry Logic

Feb 16, 2026Feb 22, 2026
6 min
16 commits
Depth 8/10
authenticationresilienceerror-handlingsecurity

Resilient Authentication: Implementing Robust Token Retry Logic

Introduction

In security-critical systems like smart lock management, authentication failures can be catastrophic. A single network hiccup or temporary server overload shouldn't lock users out of their properties. This article explores how to implement resilient token refresh mechanisms that gracefully handle transient failures while maintaining security guarantees.

The Challenge of Token Management in Critical Systems

Smart lock systems require continuous authentication validity. Unlike web applications where users can simply re-login, physical access systems must handle authentication seamlessly. Token expiration at the wrong moment could mean a guest can't enter their room or property managers lose access during emergencies.

Implementing Exponential Backoff with Circuit Breaking

A robust token refresh system combines multiple resilience patterns:

interface RetryConfig {
  maxAttempts: number;
  baseDelayMs: number;
  maxDelayMs: number;
  backoffMultiplier: number;
}

class TokenManager {
  private config: RetryConfig = {
    maxAttempts: 3,
    baseDelayMs: 1000,
    maxDelayMs: 30000,
    backoffMultiplier: 2
  };

  async refreshTokenWithRetry(): Promise<string> {
    let lastError: Error;
    
    for (let attempt = 0; attempt < this.config.maxAttempts; attempt++) {
      try {
        const token = await this.performTokenRefresh();
        await this.validateTokenClaims(token);
        return token;
      } catch (error) {
        lastError = error;
        
        if (!this.isRetryableError(error) || attempt === this.config.maxAttempts - 1) {
          throw error;
        }
        
        const delay = this.calculateBackoffDelay(attempt);
        await this.sleep(delay);
      }
    }
    
    throw lastError;
  }

  private isRetryableError(error: Error): boolean {
    // Only retry on network/server errors, not auth failures
    return error instanceof NetworkError || 
           error instanceof ServerError;
  }

  private calculateBackoffDelay(attempt: number): number {
    const exponentialDelay = this.config.baseDelayMs * 
      Math.pow(this.config.backoffMultiplier, attempt);
    
    // Add jitter to prevent thundering herd
    const jitter = Math.random() * 0.1 * exponentialDelay;
    
    return Math.min(
      exponentialDelay + jitter,
      this.config.maxDelayMs
    );
  }
}

State Preservation During Authentication Failures

Critical operations like check-in processes must maintain state consistency even when authentication temporarily fails:

interface CheckInState {
  propertyId: string;
  guestId: string;
  checkInTime: Date;
  lockCodes: string[];
  status: 'pending' | 'in_progress' | 'completed' | 'failed';
}

class ResilientCheckInManager {
  private stateStorage = new SecureStateStorage();

  async performCheckIn(request: CheckInRequest): Promise<void> {
    const checkInId = this.generateCheckInId();
    let state: CheckInState = {
      propertyId: request.propertyId,
      guestId: request.guestId,
      checkInTime: new Date(),
      lockCodes: [],
      status: 'pending'
    };

    try {
      await this.stateStorage.save(checkInId, state);
      
      state.status = 'in_progress';
      await this.stateStorage.update(checkInId, state);
      
      // This might fail due to token issues
      const lockCodes = await this.generateLockCodes(request);
      state.lockCodes = lockCodes;
      
      state.status = 'completed';
      await this.stateStorage.update(checkInId, state);
      
    } catch (error) {
      if (error instanceof AuthenticationError) {
        // Preserve state for retry, don't fail the entire operation
        await this.scheduleRetry(checkInId, state);
        throw new TransientCheckInError('Check-in will be retried automatically');
      }
      
      state.status = 'failed';
      await this.stateStorage.update(checkInId, state);
      throw error;
    }
  }

  private async scheduleRetry(checkInId: string, state: CheckInState): Promise<void> {
    const retryJob = {
      id: checkInId,
      type: 'check_in_retry',
      state,
      scheduledAt: new Date(Date.now() + 30000) // 30 second delay
    };
    
    await this.jobQueue.schedule(retryJob);
  }
}

Preventing Duplicate Operations with Idempotency

Transient failures can lead to duplicate operations. Idempotency keys ensure operations are safe to retry:

class IdempotentOperationManager {
  private operationCache = new Map<string, OperationResult>();
  
  async executeWithIdempotency<T>(
    key: string,
    operation: () => Promise<T>,
    ttlMs: number = 300000 // 5 minutes
  ): Promise<T> {
    // Check if operation already completed
    const cached = this.operationCache.get(key);
    if (cached && !this.isExpired(cached, ttlMs)) {
      return cached.result as T;
    }
    
    try {
      const result = await operation();
      
      // Cache successful result
      this.operationCache.set(key, {
        result,
        timestamp: Date.now(),
        status: 'success'
      });
      
      return result;
    } catch (error) {
      // Don't cache failures for auth errors - they should be retried
      if (!(error instanceof AuthenticationError)) {
        this.operationCache.set(key, {
          result: error,
          timestamp: Date.now(),
          status: 'error'
        });
      }
      
      throw error;
    }
  }

  private isExpired(cached: OperationResult, ttlMs: number): boolean {
    return Date.now() - cached.timestamp > ttlMs;
  }
}

Comprehensive Error Classification

Not all errors should be handled the same way. A robust system distinguishes between error types:

enum ErrorCategory {
  AUTHENTICATION = 'auth',
  AUTHORIZATION = 'authz', 
  NETWORK = 'network',
  VALIDATION = 'validation',
  BUSINESS_LOGIC = 'business',
  SYSTEM = 'system'
}

class ErrorClassifier {
  static classify(error: Error): ErrorCategory {
    if (error.message.includes('401') || error.message.includes('invalid_token')) {
      return ErrorCategory.AUTHENTICATION;
    }
    
    if (error.message.includes('403') || error.message.includes('insufficient_permissions')) {
      return ErrorCategory.AUTHORIZATION;
    }
    
    if (error instanceof TypeError && error.message.includes('fetch')) {
      return ErrorCategory.NETWORK;
    }
    
    return ErrorCategory.SYSTEM;
  }
  
  static isRetryable(category: ErrorCategory): boolean {
    return [ErrorCategory.NETWORK, ErrorCategory.SYSTEM].includes(category);
  }
  
  static getRetryStrategy(category: ErrorCategory): RetryStrategy {
    switch (category) {
      case ErrorCategory.NETWORK:
        return { maxAttempts: 3, baseDelay: 1000 };
      case ErrorCategory.AUTHENTICATION:
        return { maxAttempts: 1, baseDelay: 0 }; // Don't retry auth failures
      default:
        return { maxAttempts: 2, baseDelay: 500 };
    }
  }
}

Monitoring and Alerting

Production resilience requires observability:

class AuthenticationMetrics {
  async recordTokenRefresh(success: boolean, attemptCount: number, duration: number): Promise<void> {
    await this.metrics.increment('token_refresh_attempts', {
      success: success.toString(),
      attempt_count: attemptCount.toString()
    });
    
    await this.metrics.histogram('token_refresh_duration_ms', duration);
    
    // Alert on high failure rates
    if (!success && attemptCount >= 3) {
      await this.alerting.sendAlert({
        severity: 'high',
        message: 'Token refresh failed after maximum retries',
        context: { attemptCount, duration }
      });
    }
  }
}

Summary

Building resilient authentication for security-critical systems requires a multi-layered approach:

  1. Smart Retry Logic: Implement exponential backoff with jitter, but only retry appropriate error types
  2. State Preservation: Maintain operation state across authentication failures to enable seamless recovery
  3. Idempotency: Prevent duplicate operations with unique keys and result caching
  4. Error Classification: Distinguish between different failure modes and handle each appropriately
  5. Observability: Monitor failure patterns and alert on concerning trends

These patterns ensure that temporary authentication hiccups don't compromise user experience or system security. In smart lock systems where access is critical, such resilience can mean the difference between a minor inconvenience and a major operational failure.

Key Insights

1
Security

Authentication Resilience Without Compromise

Retry mechanisms must distinguish between retryable network errors and non-retryable security violations to maintain both availability and security

2
Reliability

State Preservation During Failures

Critical operations like check-ins preserve state across transient failures, enabling automatic recovery without user intervention

3
Architecture

Idempotent Operation Design

Using operation keys and result caching prevents duplicate actions during retry scenarios, ensuring system consistency