A chatbot that forgets everything after each message doesn't feel much like a real conversation. Without conversation memory, it can't refer back to things you've discussed earlier, so every new message feels like starting over.

In this tutorial, we'll build a Gemini Telegram bot in Python that answers questions and remembers the conversation, even after a restart. It uses python-telegram-bot to connect to Telegram, google-genai to talk to Gemini, and SQLite to store the history, so there's no separate database to set up. Gemini's free tier is enough for testing and small projects.

By the end, you'll have a working Telegram chatbot in Python that runs on your computer or on a server.

What You Need Before You Start

You need Python 3.10 or newer, a Telegram account, a Google account, and two API keys.

Telegram bot token. Open Telegram, search for @BotFather, and send /newbot. Choose a name and a username ending in bot. BotFather replies with a token. Keep it private, because anyone who has it can control your bot.

Gemini API key. Create one in Google AI Studio. The Gemini API has a free tier, but the usage limits depend on the model and can change. You can see your current limits on the Rate Limit page in AI Studio.

Set Up the Project

Create a project folder and install the libraries:

mkdir gemini-telegram-bot
cd gemini-telegram-bot
python -m venv venv
source venv/bin/activate
pip install python-telegram-bot google-genai python-dotenv

On Windows, activate the virtual environment with venv\Scripts\activate instead.

Now create a .env file in the project folder:

TELEGRAM_BOT_TOKEN=your-telegram-token
GEMINI_API_KEY=your-gemini-key
GEMINI_MODELS=gemini-3.8-flash,gemini-3.5-flash-lite,gemini-3.1-flash-lite
ALLOWED_USER_IDS=

GEMINI_MODELS is a comma-separated list with your preferred model first. The bot switches to the next one when a model reaches its usage limit or is overloaded. As of October 2026, Google's pricing page lists free-tier access for all three models above. gemini-3.8-flash is the newest Flash model, and gemini-3.5-flash-lite and gemini-3.1-flash-lite are lighter options. Model names change, so check Google's models page for the current list. If GEMINI_MODELS is missing, the bot stops at startup.

Leave ALLOWED_USER_IDS empty for now. We'll use it later to control who can use the bot.

The google-genai library reads GEMINI_API_KEY from the environment, so the key never appears in your code. If you use Git, add .env, bot.db and venv/ to your .gitignore so your keys and conversation history stay out of the repository.

Write the Gemini Telegram Bot

Create a file named bot.py and paste in this code:

import logging
import os
import sqlite3

from dotenv import load_dotenv
from google import genai
from google.genai import errors, types
from telegram import Update
from telegram.constants import ChatAction
from telegram.error import TelegramError
from telegram.ext import (
    Application,
    CommandHandler,
    ContextTypes,
    MessageHandler,
    filters,
)

load_dotenv()

logging.basicConfig(level=logging.INFO)
logging.getLogger("httpx").setLevel(logging.WARNING)
logging.getLogger("google_genai").setLevel(logging.WARNING)
logger = logging.getLogger(__name__)

MODELS = [
    model.strip()
    for model in os.environ["GEMINI_MODELS"].split(",")
    if model.strip()
]

if not MODELS:
    raise SystemExit("Set GEMINI_MODELS in your .env file.")

ALLOWED_USER_IDS = {
    int(user_id)
    for user_id in os.getenv("ALLOWED_USER_IDS", "").split(",")
    if user_id.strip()
}

HISTORY_LIMIT = 20

SYSTEM_PROMPT = (
    "You are a friendly assistant in a Telegram chat. "
    "Keep answers short and clear. Reply in plain text without Markdown."
)

client = genai.Client()

db = sqlite3.connect("bot.db")
db.execute(
    "CREATE TABLE IF NOT EXISTS messages ("
    "id INTEGER PRIMARY KEY AUTOINCREMENT, "
    "chat_id INTEGER NOT NULL, "
    "role TEXT NOT NULL, "
    "text TEXT NOT NULL)"
)
db.commit()


def is_allowed(update: Update) -> bool:
    if not ALLOWED_USER_IDS:
        return True

    return (
        update.effective_user is not None
        and update.effective_user.id in ALLOWED_USER_IDS
    )


def load_history(chat_id: int) -> list[types.Content]:
    rows = db.execute(
        "SELECT role, text FROM messages "
        "WHERE chat_id = ? ORDER BY id DESC LIMIT ?",
        (chat_id, HISTORY_LIMIT),
    ).fetchall()

    rows.reverse()

    while rows and rows[0][0] == "model":
        rows.pop(0)

    return [
        types.Content(
            role=role,
            parts=[types.Part(text=text)]
        )
        for role, text in rows
    ]


def save_message(chat_id: int, role: str, text: str) -> None:
    db.execute(
        "INSERT INTO messages (chat_id, role, text) VALUES (?, ?, ?)",
        (chat_id, role, text),
    )
    db.commit()


async def generate_reply(contents: list[types.Content]) -> str | None:
    last_error = None

    for model in MODELS:
        try:
            response = await client.aio.models.generate_content(
                model=model,
                contents=contents,
                config=types.GenerateContentConfig(
                    system_instruction=SYSTEM_PROMPT
                ),
            )

            if model != MODELS[0]:
                logger.info("Fallback model %s answered", model)

            return response.text

        except errors.APIError as e:
            if e.code != 429 and e.code < 500:
                raise

            logger.warning(
                "%s failed with error %s, trying the next model", model, e.code
            )
            last_error = e

    raise last_error


async def show_typing(
    update: Update,
    context: ContextTypes.DEFAULT_TYPE
) -> None:
    try:
        await context.bot.send_chat_action(
            chat_id=update.effective_chat.id,
            action=ChatAction.TYPING,
        )
    except TelegramError:
        pass  # the typing indicator is cosmetic, so never let it block a reply


async def start(
    update: Update,
    context: ContextTypes.DEFAULT_TYPE
) -> None:
    if not is_allowed(update):
        return

    await update.message.reply_text(
        "Hi! Send me a message and I will answer with Gemini. "
        "Use /reset to start a new conversation."
    )


async def reset(
    update: Update,
    context: ContextTypes.DEFAULT_TYPE
) -> None:
    if not is_allowed(update):
        return

    db.execute(
        "DELETE FROM messages WHERE chat_id = ?",
        (update.effective_chat.id,),
    )
    db.commit()

    await update.message.reply_text("Conversation cleared.")


async def my_id(
    update: Update,
    context: ContextTypes.DEFAULT_TYPE
) -> None:
    if update.effective_user is None:
        return

    await update.message.reply_text(
        f"Your Telegram user ID is {update.effective_user.id}"
    )


async def answer(
    update: Update,
    context: ContextTypes.DEFAULT_TYPE
) -> None:
    if not is_allowed(update):
        return

    chat_id = update.effective_chat.id
    user_text = update.message.text

    await show_typing(update, context)

    contents = load_history(chat_id) + [
        types.Content(
            role="user",
            parts=[types.Part(text=user_text)]
        )
    ]

    try:
        reply = await generate_reply(contents)

        if reply:
            save_message(chat_id, "user", user_text)
            save_message(chat_id, "model", reply)
        else:
            reply = "I could not come up with an answer. Try rephrasing."

    except errors.ClientError as e:
        if e.code == 429:
            reply = (
                "I have hit my usage limit for now. "
                "Please try again later."
            )
        else:
            logger.exception("Gemini request failed")
            reply = "Something went wrong. Please try again in a minute."

    except Exception:
        logger.exception("Gemini request failed")
        reply = "Something went wrong. Please try again in a minute."

    await update.message.reply_text(reply[:4096])


def main() -> None:
    app = (
        Application.builder()
        .token(os.environ["TELEGRAM_BOT_TOKEN"])
        .connect_timeout(30)
        .read_timeout(30)
        .write_timeout(30)
        .get_updates_connect_timeout(30)
        .get_updates_read_timeout(30)
        .build()
    )

    app.add_handler(CommandHandler("start", start))
    app.add_handler(CommandHandler("reset", reset))
    app.add_handler(CommandHandler("id", my_id))
    app.add_handler(
        MessageHandler(filters.TEXT & ~filters.COMMAND, answer)
    )

    app.run_polling()


if __name__ == "__main__":
    main()

How the bot works

SQLite stores the conversation. Every successful exchange is saved to bot.db with the Telegram chat ID, so chats stay separate and the history survives restarts. /reset deletes the saved messages for that chat. Failed requests aren't saved, so no question is left without an answer. This is short-term memory: the bot replays recent messages. For what a bot should remember long term, see how to design an agent's memory.

Only recent messages are sent. HISTORY_LIMIT sends Gemini the latest 20 messages. This keeps requests small, although it doesn't guarantee that you'll stay within the free limits.

Models fall back on limits and outages. If a request returns a 429 error (rate limit) or a 5xx error such as 503 (model overloaded), the bot tries the next model in GEMINI_MODELS. Google sets rate limits per model, so another model may still have room, and an overloaded model is often only briefly unavailable. Because the history is stored in SQLite and sent with every request, the switch works in the middle of a conversation. If every model is at its limit, the user sees a usage-limit message. Other errors skip the fallback and show a general error message. The log names the fallback model that answered, so you can see when a backup took over.

Access is controlled from .env. ALLOWED_USER_IDS lets you choose who can use the bot without editing code. We'll set it up after testing.

Replies are cut at 4,096 characters, Telegram's limit for one message. Gemini is asked for plain text because the bot doesn't handle Markdown formatting.

The typing indicator never blocks a reply. The bot shows "typing…" while Gemini works. If that call fails, the error is ignored, because the indicator is only cosmetic.

Timeouts are set to 30 seconds. The library gives up on Telegram after 5 seconds by default, which is short on a slow connection. A timeout means the reply never arrives, so main() raises the limits.

The two logging lines quiet noisy libraries. The httpx line also keeps request URLs, which can contain your bot token, out of your logs.

Run and Test the Bot

Start the bot:

python bot.py

Wait until the terminal shows INFO:telegram.ext.Application:Application started before you message the bot. Until then it isn't listening. Messages you send earlier aren't lost, because Telegram holds them and the bot answers once it starts, so replies can arrive late or all at once.

Open Telegram, find your bot, and send /start. Then test the memory:

  1. Send: My name is Sam.
  2. Send: What is my name? The bot should answer Sam.
  3. Stop the script with Ctrl+C, run python bot.py again, and ask the same question. The bot should still know, because the conversation is saved in bot.db.
  4. Send /reset and ask again. The bot should no longer know your name.
This code was tested with Python 3.14, python-telegram-bot 22.8 and google-genai 2.29.

If the bot doesn't respond, check your keys and model names, and make sure only one copy of the bot is running. Telegram allows one polling connection per bot token, so a second copy causes a conflict.

Run Your Telegram Bot on a Server

The bot only works while the script is running. If your computer sleeps, shuts down or loses its connection, the bot stops responding. A small virtual private server (VPS) keeps it online, and this bot needs very little power because it mostly waits for messages.

The steps below work on any Ubuntu or Debian VPS. As an example, we'll use VPSDime's Linux 4GB plan, which has 2 vCPUs and 4 GB of memory, more than this bot needs. A VPS is yours to maintain and secure.

1. Set up the server

Choose a location and install Ubuntu 24.04, then connect through SSH with the login details from your host. Change any temporary password, then install Python and create a user for the bot:

apt update && apt upgrade -y
apt install -y python3 python3-venv python3-pip
adduser --disabled-password --gecos '' botuser
mkdir /home/botuser/gemini-telegram-bot
chown botuser:botuser /home/botuser/gemini-telegram-bot
python3 --version

Check that Python is 3.10 or newer. The bot runs under its own user instead of root, which limits the permissions it has.

2. Copy the project files

From your computer, copy bot.py and .env to the server. Replace YOUR_SERVER_IP with the server's address:

scp bot.py .env root@YOUR_SERVER_IP:/home/botuser/gemini-telegram-bot/

Leave venv behind, because it's built on the server. Leave bot.db behind too if you want a fresh history. On the server, fix the ownership, make .env readable only by its owner, and install the packages:

chown -R botuser:botuser /home/botuser/gemini-telegram-bot
chmod 600 /home/botuser/gemini-telegram-bot/.env

su - botuser -c 'cd gemini-telegram-bot && python3 -m venv venv && venv/bin/pip install python-telegram-bot google-genai python-dotenv'

3. Keep the bot running with systemd

A systemd service starts the bot when the server boots and restarts it if it crashes. Create /etc/systemd/system/telegram-bot.service:

[Unit]
Description=Gemini Telegram bot
After=network-online.target
Wants=network-online.target

[Service]
User=botuser
WorkingDirectory=/home/botuser/gemini-telegram-bot
ExecStart=/home/botuser/gemini-telegram-bot/venv/bin/python bot.py
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target

Stop the copy running on your computer first, then start the service:

systemctl daemon-reload
systemctl enable --now telegram-bot
systemctl status telegram-bot

Follow the logs with journalctl -u telegram-bot -f, wait for the Application started line, and send the bot a message. If it answers, it's running without your computer. The database is created in the working directory, so conversations are now stored on the server.

Once everything works, switch SSH to key authentication, disable password login, and keep the server updated.

systemd restarts the bot if it crashes, but it won't tell you when something goes quietly wrong. How to monitor your automations covers simple ways to get alerted.

Limitations to Know Before Using the Bot

This setup is good for learning and for a private bot. Before other people use it, keep these in mind.

Restrict who can use it. While ALLOWED_USER_IDS is empty, anyone who finds the bot can use it, and their messages count against your Gemini quota. Send /id to the bot to get your Telegram user ID, add it to .env (separate several IDs with commas), and restart the bot:

ALLOWED_USER_IDS=123456789,987654321

On the server, restart with systemctl restart telegram-bot.

Free limits can interrupt conversations. With several active users, you may reach them. A backup model gives you more room, but when every model is at its limit, the bot stops answering until the limits reset. During testing, the main model returned rate-limit errors several times, and once a 503 "high demand" error, so the backup models earn their place.

History is stored as plain text. Anyone with access to bot.db can read it, and it grows until you delete rows. /reset clears one chat only. Don't use this setup for sensitive information.

Free-tier data use. Google's pricing page says content sent on the free tier may be used to improve its products, and content sent on the paid tier isn't. Don't send private information through the free tier.

What You've Built

You now have a Gemini Telegram bot that answers questions, remembers conversations across restarts, and can run on a server.