Background
I was tasked with developing a web interface for an e-sports FIFA tournament service (SAFA) that had been operating primarily on Discord through a Discord Bot. They wanted to extend their services to a website, and I was responsible for making it happen.
After a new user joins their Discord server, before they can interact further (e.g., searching/joining teams), they are required to update their profile using a form. After which, they were given the roles “updated-profile” and “free-agent” (indicates not being present in any team). A player if present in a team has the role “player”.
The Problem: API Rate-Limits
Whenever a user registers or logs into the website using Discord OAuth, the dashboard view presented to them is based on their roles within the Discord server. The roles dictate what actions the user can take:
- Is the user part of the Discord server? (If not, prompt them to join)
- Has the user updated their profile? (If not, show the form)
- Is the user a “free-agent”? (If yes, provide options to join/create a team)
- Is the user a “team manager”? (If yes, provide team management options)
These role details are fetched from the Discord API. However, the API endpoint for fetching roles has strict rate-limits: 5 requests per minute, with a soft reset allowing 1 request per minute after. This rate limit applies to a specific Access-Token for a Discord server.
Since user roles can change based on user interactions through their existing Discord Bot (e.g., joining the server, updating profile, joining a team), we needed to re-fetch roles each time the browser tab was re-focused or reloaded. Additionally, in a React development environment with StrictMode enabled, each component renders twice which doubled the number of API requests. This often led to rate-limiting issues.
The Solution Options
There were two main approaches to handle this issue:
-
Maintain Internal User Data: This option would involve using WebSockets with the bot to track user role changes in real-time and storing this data in a database. While this would ensure up-to-date information, it introduces significant complexity and resource overhead.
-
Cache Fetched Data from the Discord API: This simpler solution involves caching user roles from the Discord API where we use the cached data if the API is rate-limited. The trade-off is a delay in real-time data (maximum of 1 minute), but this was acceptable for our use case.
We opted for the caching approach using Redis, as it provides both efficiency and native data expiry.
The Solution: Fetch with Cache Fallback
We implemented a fetch-with-cache fallback mechanism. Here’s how it works:
1. Check for Rate-Limit Key
Before making an API call, we check if we’ve been currently rate-limited by looking for a Redis key of the pattern discord_roles_<ServerID>_<AccessToken>_retry-after
. This key is created when a previous request was rate-limited.
2. Handle Rate-Limited Requests
If this rate-limit key exists, we don’t attempt to make a new API call. Instead, we return the cached data stored in Redis with the key of pattern discord_roles_<ServerID>_<UserID>
.
3. Make API Request
If the rate-limit key isn’t found, we proceed to fetch the user’s roles from the Discord API.
- If the request is successful, we cache the data with a key of the pattern
discord_roles_<ServerID>_<UserID>
and return it. - If the request receives a rate-limit error, we create the key
discord_roles_<ServerID>_<AccessToken>_retry-after
with an expiry set to the rate-limit reset time (plus 1 second). After this, we return the previously cached data.
What I Learned and Future Considerations
While this solution worked well for our use case, I realized that using Redis opened up more possibilities for handling similar challenges elsewhere in the project. For instance, I later used Redis to solve another issue related to concurrent refresh attempts of access tokens.
In retrospect, there is another approach we could consider for future versions. Partial WebSocket Implementation: Given the requirement for very specific role updates, partial WebSocket integration in the website with Discord Gateway API could help fetch only critical updates in real-time without introducing unnecessary overhead.
Conclusion
A simple fetch-with-cache fallback system using Redis allowed us to deliver a smooth user experience while adhering to Discord’s API constraints. The combination of simplicity and effectiveness made Redis a good fit for this project.