Solving Deceiving Problems Presented by Heroku Dyno Processes
Tags: Python, Heroku, Discord Bot, Webhooks, Programming

Solution Process

Included in the data sent by the POST request was the query of the vote URL. A typical URL looks like this:

By having the user click on a link with query parameters containing the server and channel id, I could have that data readily available when I received a vote event. My implementation looked similar to the following:

With that out of the way, I was ready to host my bot on Heroku. I cleaned up my code and merged my development branch with my primary branch, which was then picked up by Heroku’s automatic deployment. I changed the webserver endpoint on Discord Bot List (DBL) to point to where my Heroku app was hosted, instead of pointing to my local machine. I waited for my bot to start up and then sent a test POST request from DBL. Immediately, I started getting an error telling me that there were no web processes running.

at=error code=H14 desc="No web processes running" method=POST path="/dblwebhook" host=_.herokuapp.com request_id=7a54e44c-5c2a-40fe-8bfc-097313c0c919 fwd="_" dyno= connect= service= status=503 bytes= protocol=https

After researching the error, it seemed that my Procfile was configured wrong. The Procfile tells Heroku what type of process to run and what file to run the process on. I had my process type set as worker, which upon further research would not work for a web server. From Heroku:

“A Heroku app’s web process type is special: it’s the only process type that can receive external HTTP traffic from Heroku’s routers. If your app includes a web server, you should declare it as your app’s web process.”

Clearly, I had just configured my process type incorrectly and all that was needed was a quick change from worker to web. And so that was exactly what I did.

# worker: python bot.py

web: python bot.py

I pushed my code to GitHub and tested my DBL vote event once again by sending a POST request to the endpoint of my Heroku app, and it worked! My bot had received the webhook and reacted accordingly. However, I wouldn’t be writing an article about a simple solution I achieved on my second attempt if there were no caveats that came with it. And unfortunately, my change from worker to web was just that.

The reason a worker process is recommended for a Python bot is that it’s constantly active. This means that the worker is always listening for a command and is able to respond to it immediately. On the other hand, a web process falls asleep after idling for some time.

As noted in the Heroku documentation,

“If an app has a free web dyno, and that dyno receives no web traffic in a 30-minute period, it will sleep.”

For obvious reasons, this wouldn’t work for a Discord bot, which receives traffic via an API and not through the web. At this point, I tried many different solutions, from creating my own web server and hosting it outside of Heroku to restructuring my code to constantly calling the dblpy API for a vote count. None of these gave me much success, and I was right back where I started: needing the HTTP routing of the web process but the uptime of the worker process. How about just running these two processes at once?

I changed my Procfile to have my processes simultaneously run my bot.py file like so:

worker: python bot.py

web: python bot.py

I pushed my code and sent out a test webhook to my app domain. The web process then fired and sent the appropriate response in the right channel. Everything seemed good there. Next, I tried a command in my Discord chat and waited for the worker process to react to it. That too worked; however, the worker process wasn’t the only one that caught the command.

Remember how the web process idles for some time after receiving web traffic? During that idle time, it acts just like the worker process and is able to receive commands from the Discord API. That meant that for thirty minutes after someone voted, any subsequent commands entered would get duplicate responses like so:

I figured that the only way of preventing a double response would be to divide the tasks of the processes. I composed a copy of my bot.py file and named it bot_web.py . This new file ran a second instance of my Discord bot, but it would only be responsible for web functions.

To link the new file with my web server, I created a new folder called cogs_web and added my dbl.py file. Now the only cogs that the web process would be able to access were the ones associated with the webserver.

I changed my Procfile to read:

worker: python bot.py

web: python bot_web.py

And I pushed my new code to GitHub to be redeployed. I once again sent my test webhook to my Heroku app and my test command to my Discord server. Everything seemed perfect this time, and I was able to use the bot as intended. But alas, I had missed a line in the Heroku documentation that would be this solution’s downfall.

“In addition to the web dyno sleeping, the worker dyno (if present) will also sleep.”

The web process essentially brings down any other processes with it. This meant that whenever the web process went to sleep, the worker process followed.