- π β’ Overview
- π¦ β’ Installation
- π β’ Usage
- π‘ β’ Response
- π» β’ Development
- π β’ Used in production
- π β’ Credits
- π β’ License
dPyStatus runs an aiohttp server inside the bot's own event loop (no thread, no extra dependency: aiohttp already ships with discord.py) and exposes the bot's stats as JSON, so an external service can tell whether the bot is online.
Key features:
- A single
GET /statusroute answering200when the bot is ready,503when it is not - Shard-aware: per-shard latency, guild count and connection state
- Optional bearer token authentication
- Your own values exposed under
extra, from sync or async callbacks - Three ways to plug it in: extension, cog or standalone server
uv add git+https://github.com/PaulBayfield/dPyStatusawait bot.load_extension("dPyStatus.extension")| Variable | Default | Description |
|---|---|---|
DPYSTATUS_HOST |
127.0.0.1 |
Interface to bind. Use 0.0.0.0 in Docker. |
DPYSTATUS_PORT |
8080 |
Port to listen on. |
DPYSTATUS_PATH |
/status |
Route of the endpoint. |
DPYSTATUS_TOKEN |
none | If set, requests must send Authorization: Bearer <token>. |
DPYSTATUS_ACCESS_LOG |
false |
Log every request. |
from dPyStatus import StatusCog
class MyBot(commands.Bot):
async def setup_hook(self) -> None:
await self.add_cog(StatusCog(self, host="0.0.0.0", port=8080, token="secret"))The server starts when the cog is loaded and stops when it is unloaded or when the bot closes.
from dPyStatus import StatusServer
class MyBot(commands.Bot):
async def setup_hook(self) -> None:
self.status = StatusServer(self, host="0.0.0.0", port=8080)
await self.status.start()
async def close(self) -> None:
await self.status.stop()
await super().close()StatusServer is also an async context manager (async with StatusServer(bot): ...), and works with a plain
discord.Client or discord.AutoShardedClient too.
Expose your own values under extra. Callbacks take no argument, may be sync or async, and must return something
JSON-serialisable. If one raises, its value is null and the error is logged on the dPyStatus logger.
status = bot.get_cog("dPyStatus").server # or your StatusServer instance
@status.extra
def version() -> str:
return "3.0.0"
@status.extra("database")
async def database_ok() -> bool:
return await bot.pool.fetchval("SELECT TRUE")
status.add_extra("restaurants", lambda: len(bot.restaurants))
status.remove_extra("restaurants")See examples/:
| File | Description |
|---|---|
extension.py |
Load the extension, configured by environment variables. |
cog.py |
Add the cog with explicit options and register extra values. |
standalone.py |
StatusServer as a context manager with an AutoShardedClient. |
check.py |
Query the endpoint from an external service (exit code 0 if up). |
GET /status (and HEAD /status) answers:
| Code | status |
Meaning |
|---|---|---|
200 |
online |
The bot is ready. |
200 |
degraded |
Ready, but some shards are down. |
503 |
starting / offline |
Not ready yet / closed. |
401 |
n/a | Missing or wrong token. |
{
"status": "online",
"ready": true,
"latency_ms": 42.1,
"ready_at": "2026-09-18T10:00:00.000000+00:00",
"uptime": 3600.123,
"timestamp": "2026-09-18T11:00:00.123000+00:00",
"user": { "id": "123456789012345678", "name": "CROUStillant" },
"stats": {
"guilds": 120,
"users": 45210,
"cached_users": 3120,
"channels": {
"total": 2400, "text": 1500, "voice": 500, "stage": 10,
"category": 350, "forum": 30, "other": 10, "threads": 80
},
"shard_count": 1
},
"shards": [{ "id": 0, "latency_ms": 42.1, "closed": false, "guilds": 120 }],
"extra": { "database": true },
"versions": { "python": "3.13.2", "discord.py": "2.7.1", "dPyStatus": "0.1.0" }
}usersis the sum ofmember_countover guilds;cached_usersislen(bot.users).- IDs are strings (they do not fit in a JavaScript number).
latency_msanduptimearenulluntil the bot has connected.
uv sync
uv run pytest
uv run ruff check . && uv run ruff format --check .dPyStatus powers the health endpoint of the CROUStillant Discord bot
(CROUStillantBot), where it is loaded as an extension
and enriched with a few extra values (maintenance flag, database connectivity and cache sizes):
await self.load_extension("dPyStatus.extension")
status = self.get_cog("dPyStatus").server
@status.extra
def maintenance() -> bool:
return self.maintenance
@status.extra
async def database() -> bool:
return await self.entities.pool.fetchval("SELECT TRUE")
@status.extra
def cache() -> dict[str, int]:
return {
"regions": len(self.cache.regions),
"restaurants": len(self.cache.restaurants),
}Using dPyStatus somewhere else? Open a pull request and add your project here.
| Person | Role |
|---|---|
| Paul Bayfield | Author & Maintainer |
dPyStatus is licensed under the Apache 2.0 License.
Copyright 2026 Paul Bayfield
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
