I was working on a Rails application that integrated with Slack.
The integration had two main responsibilities. The first was allowing users to map their Slack accounts with their profiles in our application. The second was sending notifications to users or channels whenever certain events occurred.
Everything worked well while we were developing locally, but once the application reached staging and production, we started seeing an issue that hadn’t shown up before.
Slack began returning 429 Too Many Requests.
At first, the errors were occasional, but as more people started using the application, they became more frequent. Some notifications weren’t being delivered, and user mapping would occasionally fail because the API request was being rate limited.
Finding the Bottleneck
The first thing I wanted to understand was where all these API calls were coming from.
It turned out we were requesting the Slack user list far more often than necessary.
Whenever someone wanted to map their Slack account, we called users.list to fetch every member in the workspace. That worked fine for a few users, but as usage increased, we were repeatedly asking Slack for the same information over and over again.
At the same time, our notification flow was making additional API requests whenever it needed to resolve Slack users.
Individually, none of these calls were excessive.
Together, they were enough to push us into Slack’s rate limits.
Reducing Unnecessary API Calls
The easiest win was caching the Slack user list.
Instead of calling users.list every time, we cached the response for each connected Slack workspace.
def slack_users
cache_key = "slack_users_#{account_id}"
Rails.cache.fetch(cache_key, expires_in: 10.minutes) do
slack_client.users_list["members"]
end
end
Since each connected Slack workspace has its own users, the cache was scoped by account to avoid any possibility of data leaking between workspaces.
A ten-minute cache was more than enough for our use case. Workspace members don’t change very often, so making repeated API calls simply wasn’t necessary.
Making Notifications More Reliable
The second improvement was around notifications.
Rather than looking up Slack users every time we wanted to send a message, we stored the Slack user ID after the initial mapping and reused it.
Notifications were also moved into background jobs with retries.
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)
return unless user.slack_id.present?
slack_client.chat_postMessage(
channel: user.slack_id,
text: message
)
end
end
That way, if Slack temporarily rejected a request because of rate limiting, the notification would simply be retried instead of being lost.
The Outcome
After these changes, the difference was immediately noticeable.
The number of Slack API requests dropped significantly because we were no longer fetching the same user list over and over again.
More importantly, the 429 errors disappeared.
Notifications became much more reliable, and the Slack mapping flow felt almost instant because most requests were now being served from the cache.
What I Took Away
This wasn’t really a Slack problem—it was an application design problem.
We were treating the Slack API as if it were our own database, even though every request had a cost and was subject to rate limits.
Since then, whenever I integrate with an external API, one of the first questions I ask is:
“Do I really need to make this request every single time?”
More often than not, the answer is no.
A small amount of caching can make an application faster, more reliable, and much friendlier to the services it depends on.