NapNodes

← Discord bot guides

Discord bots · updated 31 October 2026

Discord member count bot: channels with members, online and boosts (Python and JavaScript)

Stats channels (“👥 Members: 1,234”) are the first thing people see when they join a Discord server, and the bots that make them usually carry their own branding or charge for more than one counter. You build this one yourself in discord.py or discord.js: a /counter command creates the category with three channels (members, online and boosts) and the bot refreshes them every 10 minutes, which is what Discord allows. No database and no privileged intents.

What the bot does

Someone who can manage channels types /counter once. The bot creates a “📊 Server stats” category at the very top with three voice channels: “👥 Members”, “🟢 Online” and “🚀 Boosts”, each with its number. Everyone sees them, but nobody can join: they're just a sign. Voice channels are used because their names allow spaces, capitals and emojis, and they're always visible in the list without anyone opening them.

Every 10 minutes the bot asks Discord for the server's numbers and renames the channels whose number has changed. The channels it creates are saved in counters.json, so a restart doesn't lose them, and one bot can run the counters of several servers at once.

It doesn't need any privileged intent (neither “Server Members” nor “Presence”): it doesn't count anyone by hand, it uses the counts Discord already works out for every server, the same ones an invite shows (“1,234 members · 210 online”).

Rather not set it up yourself? Discord bot hosting from €1.50 a month, no contract and a full refund within 72 hours. Or create a free Discord bot, no card needed.

1. Create the bot and invite it

  1. Create the application and get the token

    At discord.com/developers/applications, “New Application”; in the “Bot” tab, “Reset Token” and copy the token. It's the bot's password: don't share it or push it to GitHub. If it's your first bot, the slow walkthrough is in your first bot in Python or in JavaScript. Leave the “Privileged Gateway Intents” off.

  2. Invite it with three permissions

    In “OAuth2” → “URL Generator”, tick “bot” and “applications.commands”, and in the permissions “View Channels”, “Manage Channels” and “Connect” (in total, the number 1049616). “Connect” is needed even though the bot never joins: to take away @everyone's permission to join a channel, Discord requires the bot to have it. The bot permissions calculator explains each one and gives you the link.

2. The bot in Python (discord.py)

Three files in one folder. The token goes in .env, without quotes. You need Python 3.10 or newer. To change what each channel says, edit LABELS: {n} is where the number goes.

requirements.txt
discord.py
python-dotenv
.env
DISCORD_TOKEN=paste-your-token-here
app.py
import json
import os
from pathlib import Path

import discord
from discord import app_commands
from discord.ext import tasks
from dotenv import load_dotenv

load_dotenv()  # reads the token from the .env file

# Each server's channels are saved next to the bot: a restart doesn't lose them.
COUNTERS = Path("counters.json")
# What each channel says. {n} is the number.
LABELS = {
    "members": "👥 Members: {n}",
    "online": "🟢 Online: {n}",
    "boosts": "🚀 Boosts: {n}",
}


def load() -> dict:
    return json.loads(COUNTERS.read_text("utf-8")) if COUNTERS.exists() else {}


def save(data: dict) -> None:
    COUNTERS.write_text(json.dumps(data, indent=2), "utf-8")


def number(n: int) -> str:
    return f"{n:,}"  # 1,234, with the thousands separator


class Bot(discord.Client):
    def __init__(self):
        # No privileged intents: the numbers are asked from Discord, not counted by hand.
        super().__init__(intents=discord.Intents.default())
        self.tree = app_commands.CommandTree(self)

    async def setup_hook(self):
        await self.tree.sync()  # publishes /counter
        update.start()


bot = Bot()


async def numbers(guild_id: int) -> dict[str, int]:
    # with_counts=True brings Discord's approximate counts: members and online.
    guild = await bot.fetch_guild(guild_id, with_counts=True)
    return {
        "members": guild.approximate_member_count or 0,
        "online": guild.approximate_presence_count or 0,
        "boosts": guild.premium_subscription_count or 0,
    }


@tasks.loop(minutes=10)  # no less: Discord only lets you rename a channel twice every 10 minutes
async def update():
    for guild_id, channels in load().items():
        try:
            counts = await numbers(int(guild_id))
            for key, channel_id in channels.items():
                channel = bot.get_channel(channel_id)
                if channel is None:
                    continue  # the channel was deleted: nothing to do
                name = LABELS[key].format(n=number(counts[key]))
                if channel.name != name:  # only if it changed: each change uses up one of the 2
                    await channel.edit(name=name, reason="Member counter")
        except discord.HTTPException as error:
            print(f"Couldn't update server {guild_id}: {error!r}")


@update.before_loop
async def before_update():
    await bot.wait_until_ready()


@bot.tree.command(name="counter", description="Creates the channels with members, online and boosts")
@app_commands.guild_only()
@app_commands.default_permissions(manage_channels=True)
async def counter(interaction: discord.Interaction):
    guild = interaction.guild
    data = load()
    if any(guild.get_channel(c) for c in data.get(str(guild.id), {}).values()):
        await interaction.response.send_message("They already exist. To start over, delete the category and type /counter again.", ephemeral=True)
        return
    await interaction.response.defer(ephemeral=True)  # creating channels can take a few seconds
    # Everyone sees the channels, but nobody can join them: they're just a sign.
    overwrites = {
        guild.default_role: discord.PermissionOverwrite(view_channel=True, connect=False),
        guild.me: discord.PermissionOverwrite(view_channel=True, manage_channels=True),
    }
    try:
        category = await guild.create_category("📊 Server stats", overwrites=overwrites, position=0)
        counts = await numbers(guild.id)
        channels = {}
        for key, text in LABELS.items():
            channel = await guild.create_voice_channel(text.format(n=number(counts[key])), category=category, overwrites=overwrites)
            channels[key] = channel.id
    except discord.Forbidden:
        await interaction.followup.send("I'm missing permissions: I need View Channels, Manage Channels and Connect.", ephemeral=True)
        return
    data[str(guild.id)] = channels
    save(data)
    await interaction.followup.send("Done: the “📊 Server stats” category is at the very top. The numbers refresh every 10 minutes.", ephemeral=True)


@bot.event
async def on_ready():
    print(f"Logged in as {bot.user}")


bot.run(os.environ["DISCORD_TOKEN"])

The same bot in JavaScript (discord.js)

package.json, .env and index.js. You need Node.js 20 or newer. Numbers are written with toLocaleString("en-GB"), which adds the thousands comma (1,234). If you write the labels in Spanish, use toLocaleString("es-ES", { useGrouping: "always" }): without useGrouping, Spanish leaves out the thousands separator until five digits (1234, but 12.345).

package.json
{
  "name": "counter-bot",
  "version": "1.0.0",
  "main": "index.js",
  "dependencies": {
    "discord.js": "^14.16.0",
    "dotenv": "^16.4.0"
  }
}
.env
DISCORD_TOKEN=paste-your-token-here
index.js
require("dotenv").config(); // reads the token from the .env file
const fs = require("node:fs");
const path = require("node:path");
const { ChannelType, Client, Events, GatewayIntentBits, InteractionContextType, MessageFlags, PermissionFlagsBits, SlashCommandBuilder } = require("discord.js");

// Each server's channels are saved next to the bot: a restart doesn't lose them.
const COUNTERS = path.join(__dirname, "counters.json");
// What each channel says. {n} is the number.
const LABELS = {
  members: "👥 Members: {n}",
  online: "🟢 Online: {n}",
  boosts: "🚀 Boosts: {n}",
};
const TEN_MINUTES = 10 * 60 * 1000; // no less: Discord only lets you rename a channel twice every 10 minutes

const load = () => (fs.existsSync(COUNTERS) ? JSON.parse(fs.readFileSync(COUNTERS, "utf8")) : {});
const save = (data) => fs.writeFileSync(COUNTERS, JSON.stringify(data, null, 2));
const number = (n) => n.toLocaleString("en-GB"); // 1,234, with the thousands separator
const label = (key, n) => LABELS[key].replace("{n}", number(n));

// No privileged intents: the numbers are asked from Discord, not counted by hand.
const client = new Client({ intents: [GatewayIntentBits.Guilds] });

async function numbers(guildId) {
  // withCounts brings Discord's approximate counts: members and online.
  const guild = await client.guilds.fetch({ guild: guildId, withCounts: true, force: true });
  return {
    members: guild.approximateMemberCount ?? 0,
    online: guild.approximatePresenceCount ?? 0,
    boosts: guild.premiumSubscriptionCount ?? 0,
  };
}

async function update() {
  for (const [guildId, channels] of Object.entries(load())) {
    try {
      const counts = await numbers(guildId);
      for (const [key, channelId] of Object.entries(channels)) {
        const channel = client.channels.cache.get(channelId);
        if (!channel) continue; // the channel was deleted: nothing to do
        const fresh = label(key, counts[key]);
        if (channel.name !== fresh) await channel.setName(fresh, "Member counter"); // only if it changed
      }
    } catch (error) {
      console.error(`Couldn't update server ${guildId}:`, error);
    }
  }
}

const command = new SlashCommandBuilder()
  .setName("counter")
  .setDescription("Creates the channels with members, online and boosts")
  .setContexts(InteractionContextType.Guild)
  .setDefaultMemberPermissions(PermissionFlagsBits.ManageChannels); // by default, only those who can manage channels

client.on(Events.InteractionCreate, async (i) => {
  if (!i.isChatInputCommand() || i.commandName !== "counter") return;
  const data = load();
  if (Object.values(data[i.guildId] ?? {}).some((id) => i.guild.channels.cache.has(id))) {
    await i.reply({ content: "They already exist. To start over, delete the category and type /counter again.", flags: MessageFlags.Ephemeral });
    return;
  }
  await i.deferReply({ flags: MessageFlags.Ephemeral }); // creating channels can take a few seconds
  // Everyone sees the channels, but nobody can join them: they're just a sign.
  const overwrites = [
    { id: i.guild.roles.everyone.id, allow: [PermissionFlagsBits.ViewChannel], deny: [PermissionFlagsBits.Connect] },
    { id: client.user.id, allow: [PermissionFlagsBits.ViewChannel, PermissionFlagsBits.ManageChannels] },
  ];
  try {
    const category = await i.guild.channels.create({ name: "📊 Server stats", type: ChannelType.GuildCategory, permissionOverwrites: overwrites, position: 0 });
    const counts = await numbers(i.guildId);
    const channels = {};
    for (const key of Object.keys(LABELS)) {
      const channel = await i.guild.channels.create({ name: label(key, counts[key]), type: ChannelType.GuildVoice, parent: category.id, permissionOverwrites: overwrites });
      channels[key] = channel.id;
    }
    data[i.guildId] = channels;
    save(data);
    await i.editReply("Done: the “📊 Server stats” category is at the very top. The numbers refresh every 10 minutes.");
  } catch (error) {
    console.error(error);
    await i.editReply("I'm missing permissions: I need View Channels, Manage Channels and Connect.");
  }
});

client.once(Events.ClientReady, async (c) => {
  await c.application.commands.set([command.toJSON()]); // publishes /counter
  console.log(`Logged in as ${c.user.tag}`);
  setInterval(update, TEN_MINUTES);
  update();
});

client.login(process.env.DISCORD_TOKEN);

3. Try it

Who sees /counter can be changed without touching the code in “Server Settings” → “Integrations” → your bot. If you delete a channel, the bot stops touching it; to start over, delete the category with its channels and type /counter again.

  1. Start the bot

    In a terminal, in the folder: pip install -r requirements.txt and python app.py (or npm install and node index.js). When it says “Logged in as…”, you're set. The command can take a few minutes to show up the first time.

  2. Create the counters

    Type /counter in any channel. In a few seconds the “📊 Server stats” category appears at the top with the three channels. Move it wherever you like: the bot only changes the names, not the position.

  3. Watch it change

    Invite someone, or leave and rejoin with another account. The number doesn't change instantly: on the bot's next round (at most 10 minutes) and sometimes one more, because Discord's counts are approximate and take a little while to catch up.

Why every 10 minutes and not instantly

Discord only lets you rename the same channel 2 times every 10 minutes. A bot that renames the channel every time someone joins or leaves runs out of renames as soon as three people arrive in a row, and from then on every change waits in the queue: the number falls further and further behind. discord.py logs it in the console as “We are being rate limited… responded with 429”.

That's why this bot does one round every 10 minutes and only renames the channel if the number changed: at most one change per channel per round, half of what Discord allows. If you lower the interval, it won't be faster: it'll stutter.

“Online” is anyone who doesn't show as offline: online, idle or do not disturb. Someone set to invisible counts as offline, just like in the member list.

If something doesn't work

“I'm missing permissions”: the bot lacks “View Channels”, “Manage Channels” or “Connect”. Invite it again with the link from the calculator, or give its role those permissions in “Server Settings” → “Roles”.

The channels were created but the name never changes: someone removed the bot from the permissions of the category or the channel. In the channel, “Edit Channel” → “Permissions”, add the bot's role with “View Channel” and “Manage Channel”. The console says “Missing Permissions” (code 50013).

/counter doesn't show up: what to check when slash commands don't appear. If the bot is online and doesn't answer, my bot isn't responding.

Counting other things

The numbers come from numbers(): add a key with its label in LABELS and its value in numbers(), and /counter will create one more channel. With what the server already has, no new permissions: the number of channels (len(guild.channels) in Python, with the server from the cache) or of roles.

Telling people apart from bots, or counting who has a role, does need the “Server Members” intent: you have to turn it on in the portal and in the code, and the bot has to go through every member. It's worth it if you already use it, for example with the welcome bot; which intent each thing needs, in the intents calculator.

YouTube or Twitch followers: those come from each site's API, not from Discord. To announce streams and new videos, the Twitch and YouTube notifications bot.

4. Keep it online all the time (free)

If the bot lives on your computer, the counters freeze as soon as you turn it off: they stay on the server with the last number, but they stop moving. It has to be hosted on a server; free and paid options, compared, in where to host your Discord bot.

NapNodes has a free 256 MB bot that isn't shut down for being idle; you renew it every 7 days with “Start” (its catches, in this guide). Create it, pick Node.js or Python in “Settings” → “Language”, upload your files in “Files” and click “Start”: the libraries in requirements.txt or package.json install themselves on startup. counters.json stays in the bot's folder between restarts. This bot uses very little, so it fits alongside others in the same one: to avoid renewing, the Mini plan (512 MB) is always on (bot prices).

Frequently asked questions

How do I show the member count in a Discord channel?

With a bot that renames a voice channel, for example to “👥 Members: 1,234”, and refreshes it every 10 minutes. The full code in Python and JavaScript is in this guide.

Why isn't my member count updating?

Because Discord only lets you rename a channel 2 times every 10 minutes. A bot that renames it on every join or leave gets stuck in the queue; renaming every 10 minutes and only when the number changes avoids it.

Does a member count bot need privileged intents?

No, if it uses the counts Discord works out for every server (members and online), like this one. You only need the members intent to tell people apart from bots or to count who has a role.

Which permissions does the counter bot need?

View channels, manage channels and connect (the permissions number is 1049616). Connect is needed so it can take away everyone's permission to join the counter channels.

Where do I keep the counter bot online all the time?

On a server. NapNodes has a free 256 MB bot that you renew with one click every week, or the Mini plan, always on.

Shall we set it up for you?

Discord bot hosting from €1.50 a month, never oversold: your RAM is yours alone. And if you'd rather start without paying, there's a free Discord bot, no card needed.

Free tool: Discord bot permissions calculator. Tick your bot's permissions and get the number and the invite link; or paste a number and see which permissions it grants.

Keep reading

All Discord bot guides, sorted →