Pitfalls with Multiple Workers

Does your bot work fine locally, but throw an error like this when you deploy it?

[2021-07-29 14:00:00 +0000] [12] [ERROR] Exception in worker process
Traceback (most recent call last):
File "/usr/local/lib/python3.9/site-packages/flask_discord_interactions/discord.py", line 273, in update_commands
    response.raise_for_status()
File "/usr/local/lib/python3.9/site-packages/requests/models.py", line 953, in raise_for_status
    raise HTTPError(http_error_msg, response=self)
requests.exceptions.HTTPError: 429 Client Error: Too Many Requests for url: https://discord.com/api/v9/applications/[...]

There are many potential pitfalls when using this library, where code can work fine with one worker process but crash when scaled horizontally.

Rate Limiting

If you’re deploying to an environment with multiple concurrent workers, you may run into a situation where each worker will independently attempt to register your application’s commands with Discord. When this happens, there’s a good chance you’ll get rate-limited!

Issue Reproduction

You can see this issue by running an example with gunicorn -w 4 or similar. This will run four workers, each one will send a separate request to the Discord API, and some of them will get rate-limited and crash.

Background

Most of the examples for this library will call discord.update_commands when the Flask app initializes. This can be useful in a development environment: every time you restart your Flask app, it will ensure that any changes in your commands are sent to Discord.

However, this becomes a problem if you want to serve your app with more than one worker. Since each worker runs in an isolated environment, there isn’t a reliable way for Flask-Discord-Interactions to “coordinate” command registration across multiple workers. Thus, this task left is up to you.

Here are some general approaches you can use to handle command registration for your app.

Approach 1: Manual Update

You can manually run discord.update_commands before deploying your app. For convenience, you could put this behind a command line argument:

import sys
from flask import Flask
from flask_discord_interactions import DiscordInteractions

app = Flask(__name__)
discord = DiscordInteractions(app)

# ... configure your app and define your commands here ...

if "register" in sys.argv:
    discord.update_commands()
    sys.exit()


if __name__ == '__main__':
    app.run()

You can run python3 app.py register (or similar) on your development machine once to register the commands with Discord, then gunicorn app:app (or similar) on your production server to serve your app without doing command registration.

This option is the most versatile, since it does not depend on any specific web server or hosting setup.

Approach 2: Server Hooks

Warning

Note that if your application relies on parallelization beyond just one worker pool, this approach probably won’t work for you. For instance, if you’re running multiple Heroku dynos / Docker containers / VM instances, each with its own separate web server, then each container will still make its own API requests, and you’ll likely still see rate-limiting. For this reason, for more complex deployments, it is recommended that you use a manual approach.

Some Python web servers provide “hooks” that allow you to execute code when certain server events happen. This usually includes when the server starts, when a new worker starts, and when the web server closes. These often must be defined in a separate configuration file. For this example, we will focus on Gunicorn.

Gunicorn allows you to specify a configuration file with the -c [filename] command-line argument. This file can define an on_starting function that will be executed exactly once when the server starts. This is where you can register your commands with Discord.

You will need to make sure that your application module does not call discord.update_commands when it is invoked:

# filename: app.py
import sys
from flask import Flask
from flask_discord_interactions import DiscordInteractions

app = Flask(__name__)
discord = DiscordInteractions(app)

# ... configure your app and define your commands here ...
# but do NOT call `discord.update_commands`

if __name__ == '__main__':
    app.run()

Then, you will need to create a configuration file for Gunicorn:

# filename: app_conf.py
from app import discord

def on_starting(server):
    discord.update_commands()

(Note that your import structure may vary depending on your application structure.)

Finally, specify your configuration file when you run Gunicorn:

$ gunicorn -c app_conf.py app:app

Custom IDs

When declaring a custom ID handler without specifying the custom ID, te DiscordInteractions.custom_handler() decorator will actually generate a custom ID string itself (a uuid.uuid4()). It will return this custom ID string in place of the function.

This strategy works great for development, but can lead to some frustrating behavior in production:

  • Every time your app is restarted, old custom handlers will no longer function. This is likely desirable in development, but can cause issues in production.

  • If you deploy multiple instances/workers of your application, then they will not share the same custom IDs. This can lead to many issues, such as failure for one worker to process Interactions related to messages sent on another worker.

To avoid these issues, it is recommended that you specify a specific custom ID in these scenarios.

Instead of this:

@discord.custom_handler()
def handle_my_interaction(ctx, interaction_id, current_count: int):
    ...

Try this:

@discord.custom_handler("handle my cool interaction")
def handle_my_interaction(ctx, interaction_id, current_count: int):
    ...

But be mindful: your custom ID, plus whatever state you want to add (see Storing State), must fit within 100 characters!