Scheduled tasks in a Discord bot
Daily reminders, hourly stat updates, a weekly leaderboard reset. The code is short; the parts that go wrong are starting it before the bot is ready, time zones, and what happens when the bot restarts.
7 minute read
discord.py: tasks.loop
discord.ext.tasks runs a coroutine on a schedule and handles the waiting and reconnects for you.
import datetime
import os
import discord
from discord.ext import commands, tasks
CHANNEL_ID = 123456789012345678
MIDNIGHT_UTC = datetime.time(hour=0, minute=0, tzinfo=datetime.timezone.utc)
class Reminders(commands.Cog):
def __init__(self, bot: commands.Bot):
self.bot = bot
self.daily.start()
def cog_unload(self):
self.daily.cancel()
@tasks.loop(time=MIDNIGHT_UTC)
async def daily(self):
channel = self.bot.get_channel(CHANNEL_ID)
if channel:
await channel.send("A new day. Drink some water.")
@daily.before_loop
async def before_daily(self):
await self.bot.wait_until_ready()
async def setup(bot: commands.Bot):
await bot.add_cog(Reminders(bot))
@tasks.loop(minutes=10)runs every ten minutes from when it starts.@tasks.loop(time=...)runs at fixed times of day, and takes a list for several:time=[datetime.time(9), datetime.time(21)].- Always give the time a time zone. A
datetime.timewithouttzinfois treated as UTC, which is fine if you meant UTC and an hour or two out if you did not. Servers usually run on UTC or a data-centre time zone, not yours. before_loopwithwait_until_ready()stops the first run happening before the bot has logged in, whenget_channelstill returnsNone.- An exception inside the loop stops the loop silently unless you handle it. Wrap the body in
try/exceptand log, or add an@daily.errorhandler.
discord.js: setInterval, or node-cron for clock times
For "every N minutes", plain setInterval is enough. Start it once the client is ready:
const { Client, Events, GatewayIntentBits } = require("discord.js");
const client = new Client({ intents: [GatewayIntentBits.Guilds] });
const CHANNEL_ID = "123456789012345678";
client.once(Events.ClientReady, () => {
setInterval(async () => {
try {
const channel = await client.channels.fetch(CHANNEL_ID);
await channel.send("Hourly check-in.");
} catch (err) {
console.error("hourly task failed:", err);
}
}, 60 * 60 * 1000);
});
client.login(process.env.BOT_TOKEN);
For "every day at 09:00", use node-cron (npm install node-cron) and give it the time zone:
const cron = require("node-cron");
client.once(Events.ClientReady, () => {
cron.schedule("0 9 * * *", async () => {
const channel = await client.channels.fetch(CHANNEL_ID);
await channel.send("Good morning!");
}, { timezone: "Europe/London" });
});
Using client.once rather than client.on matters: the ready event can fire again after a reconnect, and on would start a second timer each time.
Restarts: the schedule lives in memory
A timer starts counting when the bot starts. Restart it at 10:59 and an hourly task next runs at 11:59, not 11:00. Two consequences:
- Use clock times for things people expect at a time.
tasks.loop(time=...)and cron schedules fire at the time regardless of when the bot started. - A run missed while the bot was down is simply missed. If that matters (a weekly reset, a reminder someone set), store the next due time in a file or database and, on start, run anything that is overdue.
# the pattern, in Python
last_reset = load_from_db("last_weekly_reset")
if now - last_reset > datetime.timedelta(days=7):
await do_weekly_reset()
save_to_db("last_weekly_reset", now)
User reminders ("remind me in 3 days") should always work this way: save the time, check every minute for anything due. A three-day sleep is lost the moment the bot restarts.
Do not hammer Discord
- Editing a message or renaming a channel every few seconds to show a live counter gets rate limited quickly; channel renames are limited to two every ten minutes. Update every few minutes instead. How rate limits work.
- A loop that sends to hundreds of channels at once should pace itself. discord.py and discord.js both queue requests for you, but a job that sends 500 messages still takes as long as the limits say.