This implementation prevents rapid repeated renewal attempts that can spam the network and overload the blockchain. It enforces a minimum time gap (cooldown period) between renewal attempts.
- Users or systems could click the retry button multiple times rapidly
- This causes unnecessary network traffic and blockchain load
- Multiple failed attempts in quick succession degrade system performance
- No tracking mechanism existed for renewal attempt frequency
File: backend/scripts/014_add_renewal_cooldown.sql
last_renewal_attempt_at(TIMESTAMP): Tracks when the last renewal attempt occurredrenewal_cooldown_minutes(INTEGER): Configurable cooldown period (default: 5 minutes)
check_renewal_cooldown(): Validates if cooldown period is activeupdate_last_renewal_attempt(): Records renewal attempt timestamp
idx_subscriptions_last_renewal_attempt: Efficient cooldown status queries
File: backend/src/services/renewal-cooldown-service.ts
checkCooldown(subscriptionId, customCooldownMinutes?)
- Returns:
CooldownCheckResult - Validates if a renewal can proceed
- Calculates remaining cooldown time
- Returns next allowed retry time
recordRenewalAttempt(subscriptionId, success, errorMessage?, attemptType?)
- Records all renewal attempts (successful or failed)
- Updates
last_renewal_attempt_attimestamp - Logs attempt type: 'automatic', 'manual', 'retry'
setCooldownPeriod(subscriptionId, cooldownMinutes)
- Allows per-subscription cooldown customization
- Validates period: 0-1440 minutes
- Returns previous and new settings
getCooldownConfig(subscriptionId)
- Retrieves cooldown configuration
- Returns last attempt time and next allowed retry time
resetCooldown(subscriptionId)
- Admin function to immediately allow retry
- Sets
last_renewal_attempt_atto null
checkRenewalCooldown(): Check status endpointretryBlockchainSync(): Enforce cooldown during retry attempts- Records attempt status (success/failure)
- GET
/api/subscriptions/:id/cooldown-status: Check cooldown status - POST
/api/subscriptions/:id/retry-sync: Retry with cooldown enforcement- Returns 429 (Too Many Requests) if cooldown active
- Includes
retryAfterheader
- Records renewal attempts from blockchain events
- Updates
last_renewal_attempt_aton success/failure - Tracks attempt type as 'automatic' for automated processes
const cooldownStatus = await renewalCooldownService.checkCooldown(subscriptionId);
if (cooldownStatus.canRetry) {
// Proceed with renewal
} else {
console.log(`Wait ${cooldownStatus.timeRemainingSeconds} seconds`);
// Show user: "Please wait 3 minutes 42 seconds"
}// After successful renewal
await renewalCooldownService.recordRenewalAttempt(
subscriptionId,
true,
undefined,
'manual'
);
// After failed renewal
await renewalCooldownService.recordRenewalAttempt(
subscriptionId,
false,
'Network timeout',
'retry'
);async retryBlockchainSync(userId, subscriptionId) {
// Automatically checks and enforces cooldown
const result = await subscriptionService.retryBlockchainSync(
userId,
subscriptionId
);
// Throws error if cooldown active: "Cooldown period active. Please wait X seconds"
}try {
await fetch(`/api/subscriptions/${id}/retry-sync`, { method: 'POST' });
} catch (error) {
if (error.status === 429) {
// Too Many Requests - cooldown active
const retryAfter = error.headers.get('retry-after');
showMessage(`Try again in ${retryAfter} seconds`);
}
}- 5 minutes (300 seconds)
- Configurable per subscription
- Minimum: 0 minutes (no cooldown)
- Maximum: 1440 minutes (24 hours)
// Set 10-minute cooldown for high-volume subscription
await renewalCooldownService.setCooldownPeriod(subscriptionId, 10);All attempts are tracked in subscription_renewal_attempts table:
- Timestamp of attempt
- Success/failure status
- Error message (if failed)
- Attempt type (automatic/manual/retry)
automatic: Scheduled or blockchain-triggered renewalsmanual: User-initiated from UIretry: Retry after failure
Status: 429 (Too Many Requests)
Body: {
"success": false,
"error": "Cooldown period active. Please wait 180 seconds before retrying.",
"retryAfter": 180
}
Status: 500
Body: {
"success": false,
"error": "Failed to check cooldown: ..."
}
File: backend/tests/renewal-cooldown-service.test.ts
- ✅ No previous attempt (canRetry = true)
- ✅ Cooldown active (canRetry = false)
- ✅ Cooldown expired (canRetry = true)
- ✅ Record successful attempt
- ✅ Record failed attempt with error
- ✅ Set custom cooldown period
- ✅ Validate cooldown period bounds
- ✅ Reset cooldown
- ✅ Retrieve cooldown config
- ✅ Integration: Rapid retry prevention workflow
Run migration to add cooldown support:
# Via Supabase dashboard:
# Execute SQL file: backend/scripts/014_add_renewal_cooldown.sql
# Or via CLI:
supabase db pushidx_subscriptions_last_renewal_attemptensures O(1) cooldown lookups- Prevents n+1 queries for batch renewal checks
- Single database lookup per cooldown check
- In-memory time calculations (no additional DB queries)
- Stateless design: no session/cache dependencies
- Works across distributed systems
- Safe for horizontal scaling
// Immediately allow retry (use with caution)
await renewalCooldownService.resetCooldown(subscriptionId);// Force retry ignoring cooldown
await subscriptionService.retryBlockchainSync(
userId,
subscriptionId,
forceBypass = true // Admin flag
);- Exponential Backoff: Increase cooldown after consecutive failures
- Rate Limiting: Limit total attempts per subscription per day
- Metrics Dashboard: Track renewal attempt patterns
- Alerts: Notify admins of suspicious attempt patterns
- Adaptive Cooldown: ML-based cooldown duration optimization
- Database:
backend/src/config/database.ts - Logger:
backend/src/config/logger.ts - Subscription Service:
backend/src/services/subscription-service.ts - Subscription Routes:
backend/src/routes/subscriptions.ts - Event Listener:
backend/src/services/event-listener.ts - Types:
backend/src/types/risk-detection.ts