🚨 The Problem

I was working on a Rails app that integrated with Slack for two main features:

  1. Mapping Slack users to our HR app
  2. 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.list and chat.postMessage can 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.