π¨ The Problem
I was working on a Rails app that integrated with Slack for two main features:
- Mapping Slack users to our HR app
- Sending notifications to Slack users or channels
Everything worked fine locally, but in staging and production, Slack rate-limit errors (429 Too Many Requests) started appearing:
- Mapping users required fetching the Slack user list repeatedly
- Sending notifications in bulk triggered multiple API requests in a short time
- Some requests failed, causing unreliable notifications and mapping
π Root Cause
Slack imposes strict rate limits per API method.
users.listandchat.postMessagecan only be called so many times per minute- Multiple requests across accounts and users could easily exceed these limits
Our app was hitting these limits because:
- Each user mapping request called
users.list - Notifications accessed Slack IDs repeatedly instead of using cached or stored data
- No mechanism existed to handle failures gracefully
The solution combined account-scoped caching for Slack users and background jobs for notifications, with retry and logging mechanisms.
π οΈ The Solution
Step 1: Cache Slack Users by Account
We cached Slack user lists per account to avoid cross-account conflicts:
def slack_users
cache_key = "slack_users_#{account_id}" # unique per account
Rails.cache.fetch(cache_key, expires_in: 10.minutes) do
slack_client.users_list["members"]
end
end
- Avoids repeated API calls
- Cache expiration ensures data freshness
- Used cached data to map Slack users to HR app users
Step 2: Optimize Notifications
Notifications were sent via a background job with retry and logging:
class SlackNotificationJob < ApplicationJob
queue_as :default
retry_on Slack::Web::Api::Errors::TooManyRequests, wait: :exponentially_longer, attempts: 5
retry_on Slack::Web::Api::Errors::SlackError, wait: 5.seconds, attempts: 3
def perform(user_id, message)
user = User.find(user_id)
# Use Slack ID stored in DB
slack_id = user.slack_id
return unless slack_id.present? # skip if no Slack ID
begin
slack_client.chat_postMessage(channel: slack_id, text: message)
rescue Slack::Web::Api::Errors::SlackError => e
Rails.logger.error("Failed to send Slack message to user #{user.id}: #{e.message}")
raise e # let the job retry based on retry_on
end
end
end
- Uses cached DB Slack IDs, minimizing API calls
- Handles failures and rate limits automatically
- Notifications are reliable and scalable
π Results
β
Slack API calls reduced dramatically.
β
Rate-limit errors eliminated.
β
User mapping became instantaneous.
β
Notifications became reliable and scalable.
π§© Key Takeaways
- Caching external API data reduces unnecessary calls and prevents rate-limit issues
- Use background jobs with retries and logging to handle failures gracefully
- Always balance freshness vs efficiency when caching external data
π Final Thoughts
By caching Slack data and optimizing notifications:
- Mapping Slack users became fast and accurate
- Notifications became reliable
- The app scaled gracefully without hitting Slackβs limits
This shows the importance of respecting external system constraints while designing Rails apps.