aiogram files
Three ways to give Telegram a file, and one way that costs nothing.
python skills/using-aiogram/tools/get-type.py PhotoSize
python skills/using-aiogram/tools/get-method.py sendDocument
when to use this skill
- sending a photo, document, video, audio, or voice message
- re-sending something the bot already sent
- downloading what a user uploaded
- handling files too large to hold in memory
- a
file_idthat works for one bot but not another
the four sources
from aiogram.types import BufferedInputFile, FSInputFile, URLInputFile
from_disk = FSInputFile("report.pdf", filename="Monthly report.pdf")
from_memory = BufferedInputFile(b"col1,col2\n1,2\n", filename="data.csv")
from_url = URLInputFile("https://example.com/image.jpg", filename="image.jpg")
by_id = "AgACAgIAAxkBAAExample" # a plain str is treated as a file_id or an http URL
assert from_disk.filename == "Monthly report.pdf"
assert isinstance(by_id, str)
Verified constructors:
FSInputFile(path, filename=None, chunk_size=65536)
BufferedInputFile(file: bytes, filename: str, chunk_size=65536)
URLInputFile(url, headers=None, filename=None, chunk_size=65536, timeout=30, bot=None)
| Source | Cost | Use when |
|---|---|---|
file_id (str) |
no upload at all | the bot has sent or received this file before |
plain http(s) URL (str) |
Telegram fetches it | a public URL Telegram can reach |
FSInputFile |
uploads, streamed from disk | a local file |
URLInputFile |
your bot downloads, then uploads | the URL needs headers/auth, or Telegram cannot reach it |
BufferedInputFile |
uploads from RAM | generated content |
FSInputFile streams in chunk_size blocks, so a 400 MB file does not become 400 MB of
memory. BufferedInputFile holds everything in RAM by definition — use it for generated
CSVs and images, not for large media.
BufferedInputFile.from_file(path, filename=None, chunk_size=...) also exists, but it
reads the whole file into memory; prefer FSInputFile.
sending
from aiogram import Router
from aiogram.types import BufferedInputFile, FSInputFile, Message
from aiogram.utils.formatting import Bold, Text
router = Router(name="send-files")
@router.message()
async def send(message: Message) -> None:
await message.answer_document(FSInputFile("report.pdf"))
await message.answer_photo(
"https://example.com/chart.png",
**Text("Chart for ", Bold("July")).as_caption_kwargs(),
)
await message.answer_document(
BufferedInputFile(b"a,b\n1,2\n", filename="data.csv"),
caption="Raw export",
)
Captions are limited to 1024 characters after entity parsing; message text allows 4096. Long explanations go in a following text message, not the caption.
receiving and downloading
from aiogram import Bot, F, Router
from aiogram.types import Message
router = Router(name="receive-files")
@router.message(F.document)
async def got_document(message: Message, bot: Bot) -> None:
document = message.document
if document is None:
return
await bot.download(
document,
destination=f"/tmp/{message.chat.id}-{message.message_id}-{document.file_unique_id}",
)
buffer = await bot.download(document) # BinaryIO when destination is omitted
if buffer is not None:
buffer.read()
@router.message(F.photo)
async def got_photo(message: Message, bot: Bot) -> None:
"""Choose the largest pixel area explicitly."""
if message.photo:
photo = max(message.photo, key=lambda item: item.width * item.height)
await bot.download(photo, destination=f"/tmp/{message.chat.id}-{message.message_id}.jpg")
Verified signatures:
Bot.download(file: str | Downloadable, destination=None, timeout=30,
chunk_size=65536, seek=True) -> BinaryIO | None
Bot.download_file(file_path: str | Path, destination=None, timeout=30,
chunk_size=65536, seek=True) -> BinaryIO | None
download accepts any Downloadable — Document, PhotoSize, Video, Audio,
Voice, Sticker, VideoNote, Animation — or a file_id string, and resolves the
file path itself. download_file takes an already-resolved file_path from get_file.
Omitting destination returns a BinaryIO holding the whole file in memory. Pass a path
or an open file object for anything large.
the download limits
- 20 MB maximum cloud Bot API download. Larger files raise
TelegramBadRequest: file is too big. A self-hosted local Bot API server supports downloads without this size limit. - The
file_pathfromget_fileis valid for at least one hour. Do not persist it — persist thefile_idand callget_fileagain when needed.
file_id and file_unique_id
This trips up almost everyone once:
file_id |
file_unique_id |
|
|---|---|---|
| scope | one bot | global |
| can download | yes | no |
| stable | may change for the same file | stable |
| use for | re-sending, caching | deduplication, storage keys |
Consequences:
- A
file_idcaptured by bot A is meaningless to bot B. Sharing ids across bots does not work. Token rotation for the same bot is not a documented reason to invalidate file ids. file_unique_idcannot be turned back into a file. It identifies, it does not fetch.- Store both:
file_unique_idas your key,file_idas the cheap re-send handle.
caching to skip uploads
aiogram ships no file cache. Re-sending by file_id is dramatically cheaper than
re-uploading, so caching is worth the twenty lines:
from aiogram import Bot
from aiogram.types import FSInputFile, InputFile, Message
class FileIdCache:
"""Upload once, re-send by id forever after. Ids are bot-scoped: key by bot id."""
def __init__(self) -> None:
self._ids: dict[tuple[int, str], str] = {}
def get(self, bot: Bot, key: str) -> str | None:
return self._ids.get((bot.id, key))
def remember(self, bot: Bot, key: str, file_id: str) -> None:
self._ids[(bot.id, key)] = file_id
cache = FileIdCache()
async def send_cached_photo(bot: Bot, chat_id: int, key: str, path: str) -> Message:
cached = cache.get(bot, key)
source: str | InputFile = cached if cached else FSInputFile(path)
message = await bot.send_photo(chat_id=chat_id, photo=source)
if not cached and message.photo:
cache.remember(bot, key, message.photo[-1].file_id)
return message
Use a real store (Redis, a table) rather than a dict once you have more than one process. Key by bot identity and content version; invalidate a stale entry on a confirmed invalid file-id error, rather than assuming all ids expired when the token was rotated.
thumbnails and streaming
from aiogram import Router
from aiogram.types import FSInputFile, Message
router = Router(name="video")
@router.message()
async def send_video(message: Message) -> None:
await message.answer_video(
FSInputFile("clip.mp4"),
thumbnail=FSInputFile("thumb.jpg"),
supports_streaming=True,
width=1280,
height=720,
duration=42,
)
supports_streaming=True lets clients play before the download finishes. Supplying
width, height, and duration supplies metadata; it does not guarantee that Telegram
will skip transcoding.
Thumbnails must be JPEG, under 200 kB, and at most 320×320.
which send method
| Content | Method | Notes |
|---|---|---|
| photo | answer_photo |
Telegram re-encodes and strips metadata |
| any file, unchanged | answer_document |
use when the bytes must survive intact |
| video | answer_video |
pass supports_streaming |
| looping muted video | answer_animation |
GIF-style |
| music | answer_audio |
shows a player, performer / title |
| voice message | answer_voice |
OGG/Opus, MP3, or M4A |
| round video | answer_video_note |
square, ≤60 s |
| sticker | answer_sticker |
WEBP/TGS/WEBM |
Sending a photo as answer_photo compresses it. When the user needs the original file —
a scan, a design asset — send it as a document.
checklist
- re-sends use
file_id, never a re-upload file_unique_idused as the storage key,file_idas the send handle- caches keyed by bot id
- large local files sent with
FSInputFile, notBufferedInputFile - downloads over ~20 MB expected to fail, and handled
file_pathfromget_filenever persisted- choose photo size by pixel area when the largest size is required
- captions kept within 1024 characters
see also
aiogram-media-groups— albumsaiogram-errors—file is too big,wrong file identifieraiogram-streaming— drafts and rich messagesskills/using-aiogram/references/telegram-quirks.md— the full limit table