Free Hosting

Reducing Memory Usage in Python Bots

Cut the memory footprint of discord.py and aiogram bots — cache and chunking settings, persistent FSM storage, lazy imports and leak hunting with tracemalloc.

On this page
  1. Measure before optimising
  2. discord.py: the big three settings
  3. aiogram and other Telegram frameworks
  4. General Python techniques
  5. Hunting a leak with tracemalloc
  6. What not to do
  7. When the answer is more RAM
  8. Summary

Python bots are usually light, but a few defaults and habits can quietly push them past a small plan’s memory limit. This guide covers library settings for discord.py and aiogram, general Python techniques, and how to find a leak when memory keeps climbing.

Measure before optimising

Log memory over time so you know what you’re fixing:

import asyncio
import logging
import resource


async def log_memory():
    while True:
        peak_mb = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss / 1024   # KB on Linux
        logging.info("peak RSS %.0f MB", peak_mb)
        await asyncio.sleep(600)

ru_maxrss reports the peak resident memory. For the current value, psutil.Process().memory_info().rss is handy if you’re happy to add the dependency. Compare steady-state numbers after the bot has run for a while, and watch the memory graph in your hosting panel.

discord.py: the big three settings

1. The message cache

discord.py keeps the last 1,000 messages by default, across all channels. In busy servers that’s a lot of objects. If you don’t need cached messages (for example, to log edits and deletions), turn it off or shrink it:

bot = commands.Bot(
    command_prefix=commands.when_mentioned,
    intents=intents,
    max_messages=None,       # or a small number like 100
)

2. Member caching

With the members intent enabled, discord.py can cache every member it learns about. Control it with MemberCacheFlags:

intents = discord.Intents.default()
intents.members = True

bot = commands.Bot(
    command_prefix=commands.when_mentioned,
    intents=intents,
    member_cache_flags=discord.MemberCacheFlags.none(),   # or .from_intents(intents) for defaults
    chunk_guilds_at_startup=False,
)

chunk_guilds_at_startup=False is important: by default, with the members intent, discord.py requests the full member list of every guild at startup. On a bot in many large servers, that alone can use more memory than a free plan has — and makes startup slow. With chunking off, members are fetched on demand (await guild.fetch_member(id)) instead.

3. Intents

Request only what your features use. The presence intent is by far the most expensive — it streams status updates for every online member. Intents.default() excludes privileged intents; for the leanest bot, start from Intents.none() and add only what you need. See gateway intents explained.

aiogram and other Telegram frameworks

Telegram bots are lighter by nature — there’s no guild cache — but two things grow:

  • In-memory FSM storage. aiogram’s default MemoryStorage keeps every user’s conversation state and data forever. Each abandoned conversation stays in memory. For production, use RedisStorage, which also survives restarts, or clear state reliably when conversations end.
  • Your own dictionaries. Per-user caches, rate-limit tables and “seen” sets grow with your user count. Give them expiry, or move them to a database.

The same applies to telebot’s state storage and grammY-style session stores. See storing Telegram bot state.

General Python techniques

Reuse HTTP sessions

Creating a new aiohttp.ClientSession for every request wastes memory and connections, and leaves “Unclosed client session” warnings. Create one session at startup and reuse it:

class MyBot(commands.Bot):
    async def setup_hook(self):
        self.http_session = aiohttp.ClientSession()

    async def close(self):
        await self.http_session.close()
        await super().close()

Bound your caches

functools.lru_cache(maxsize=None) is an unbounded cache — every distinct argument stays in memory forever. Always set a maxsize, or use a TTL cache (the cachetools package provides TTLCache).

Stream, don’t slurp

Reading a large file with f.read() or json.load() puts the whole thing in memory. Process files line by line, use generators instead of building big lists, and paginate database queries rather than fetching every row:

# Instead of: rows = await conn.fetch("SELECT * FROM big_table")
async with conn.transaction():
    async for row in conn.cursor("SELECT id, score FROM big_table"):
        process(row)

Import heavy libraries lazily

Some libraries cost tens of megabytes just to import — data-science stacks, image libraries, machine-learning SDKs. If only one command uses them, import inside that command:

@bot.tree.command()
async def chart(interaction: discord.Interaction):
    import matplotlib
    matplotlib.use("Agg")
    import matplotlib.pyplot as plt
    ...

To see what your imports cost, run python -X importtime main.py 2> imports.log and look for the largest cumulative times — they’re usually also the heaviest modules.

Close images and files

Pillow Image objects hold decoded pixel data. Use them in with blocks, or call .close(), especially in commands that generate images on every use.

Use slots for many small objects

If you keep thousands of instances of a class (cached user records, game pieces), defining __slots__ removes the per-instance dictionary and can roughly halve their memory:

class UserStats:
    __slots__ = ("user_id", "xp", "level")
    def __init__(self, user_id, xp, level):
        self.user_id, self.xp, self.level = user_id, xp, level

Hunting a leak with tracemalloc

If memory climbs steadily and never levels off, something is being retained. Python’s built-in tracemalloc shows where memory is allocated:

import tracemalloc
tracemalloc.start(25)

snapshot_before = tracemalloc.take_snapshot()
# ... let the bot run for a while ...
snapshot_after = tracemalloc.take_snapshot()

for stat in snapshot_after.compare_to(snapshot_before, "lineno")[:10]:
    print(stat)

Expose this through an owner-only command that takes a snapshot and compares it with the previous one. The lines at the top of the comparison — growing with every snapshot — point straight at the leak. Typical culprits: lists appended to on every event, tasks created but never finished, and callbacks registered repeatedly.

tracemalloc itself adds overhead, so enable it only while investigating.

What not to do

  • Don’t call gc.collect() on a timer hoping to fix memory. CPython frees most objects immediately through reference counting; if memory isn’t freed, something still references it.
  • Don’t cap memory by restarting the bot every hour. It hides leaks, drops the gateway connection and loses in-memory state.

When the answer is more RAM

Some bots legitimately need more memory — large servers with member-heavy features, bots that hold embeddings or models, image generation at scale. If you’ve trimmed caches and fixed leaks and the bot still sits near its limit, upgrade. Kerit Cloud’s Discord and Telegram plans resize in place, so your files, database and environment variables stay put.

Summary

For discord.py, shrink or disable the message cache, control member caching with MemberCacheFlags, turn off chunk_guilds_at_startup and request only the intents you use. For Telegram bots, move FSM and session state out of memory. Everywhere: reuse HTTP sessions, bound your caches, stream large data, import heavy libraries lazily, close images, and use tracemalloc to find anything that keeps growing.