Plugins share a container to cut overhead: pooling is how the marketplace runtime is deployed today, so assume your plugin runs pooled (what varies per plugin is pool size, which the platform scales for you). Your code runs unchanged; what changes is the runtime shape:
@plugin.on_readyfires per server, not per boot. It runs once per (worker, server) right before that server's first event reaches you, with a tenant-scopedctx— the designed place for per-server init like KV defaults or SQL schema bootstrap.- Schedules run server-side, from your manifest. Declare each
@plugin.crontask in the manifest's"cron"array and the platform fires it per installed server (UTC, 5-minute floor, up to 5 entries; delivery is at-least-once and missed ticks are not replayed, so keep handlers idempotent). Interval-style@plugin.scheduletasks do not run pooled — use cron or the event-driven pattern below. - Module globals are unreliable. The
Contextis rebuilt per event, your plugin runs on more than one worker, and workers restart freely. Anything that must survive belongs inctx.kvorctx.ephemeral. - Tighter limits: 24 MB memory and 0.1 vCPU per plugin (vs 64 MB / 0.25 solo).
Periodic work without schedules
For work tighter than the 5-minute cron floor, or when you'd rather skip the manifest entry, ride on event traffic and throttle with a cooldown — on an active server this runs your job roughly once per interval:
@plugin.on_event("message_create")
def on_message(ctx: Context, event: dict):
# ... your normal handling ...
# Piggyback: roll the daily summary at most once per 24h.
if not ctx.ephemeral.cooldown_check("daily_summary")["active"]:
ctx.ephemeral.cooldown_set("daily_summary", ttl_seconds=86400)
post_daily_summary(ctx)
Quiet servers produce no events, so nothing fires — which is usually what you want (no one is reading the summary anyway). When your feature genuinely needs wall-clock delivery, declare it in the manifest "cron" array instead: those fire on schedule whether or not the server is chatting.