Reusing file_ids so your Telegram bot never re-uploads media
The upload you didn’t need to make
Every time a bot sends a photo, video, document, or voice note by uploading raw bytes, Telegram has to receive that file, store it, and hand back a reference. If your bot sends the same welcome video to five hundred users, and it uploads the file five hundred times, you’re paying for four hundred and ninety-nine uploads you didn’t need. Telegram already solved this with file_id, but a lot of bots don’t use it correctly, and end up re-uploading media they already sent an hour ago.
This is the kind of detail that matters more than it looks once a bot is running behind a proxy or on a metered connection, which is most of the setups we deal with day to day. An unnecessary re-upload isn’t just slow, it’s bytes going out through infrastructure you’re paying for, on every single send.
What a file_id actually is
When your bot sends media with an upload (multipart file attached to sendPhoto, sendVideo, sendDocument, and so on), Telegram’s response includes a file_id string on the resulting message object. For a photo you get an array of PhotoSize objects, each with its own file_id, because Telegram generates several resolutions and keeps a thumbnail alongside the original. For a video, document, audio file, or voice note, you get a single file_id under the corresponding field on the message.
That string is a pointer to a file already sitting on Telegram’s servers. Pass it back as the photo, video, or document parameter on a future send call, instead of attaching a file, and Telegram just looks up the existing copy and delivers it. No bytes leave your server. The request is a small JSON call, not a multipart upload.
Alongside file_id, messages also carry a file_unique_id. That one identifies the underlying file content itself, and it stays the same no matter which bot or user is looking at it. You can’t use file_unique_id to send or download anything, but it’s the right key for deduplication, because two different bots (or the same bot at different times) can end up with two different file_id values for what is, byte for byte, the same file.
Why the scoping rules trip people up
The part that catches people off guard is that a file_id is tied to the bot that received it. A file_id your bot got back from sendVideo will not resolve for a different bot’s token, even if both bots are owned by the same person and both are members of the same chat. If you’re running more than one bot, whether for testing versus production, or because you split load across bot accounts, you cannot share a file_id cache between them. Each bot needs its own.
It’s also not something you should assume is permanent. Telegram doesn’t publish a fixed expiry, but a file_id can stop resolving, most commonly because the source file was deleted, or because enough time and reshuffling on Telegram’s storage side has passed. In practice it holds up well for ordinary use, files sent a few weeks or months back keep working fine, but “in practice it works” is different from “guaranteed forever,” and your code should treat it that way.
Building a cache instead of trusting memory
The fix is to stop treating file_id as something you read off a message and throw away. Store it the moment you get it back from an upload, keyed by something stable on your side, a content hash of the source file, or a filename plus a version number if the file never changes in place. A simple table works:
- source reference (hash or filename)
- file_id
- file_unique_id
- media type (photo, video, document, etc, since the same source file sent as a photo and as a document gets separate file_ids)
- bot identifier, if you run more than one
Before your bot uploads anything, it checks this table first. If there’s a hit, it sends by file_id. If there’s a miss, it uploads once, and writes the result back to the table so the next send is a cache hit.
The media type field matters more than people expect. If you send an image with sendPhoto, Telegram compresses it and the resulting file_id is for that compressed version. Send the same file with sendDocument and you get an entirely different file_id pointing at the untouched original. They’re not interchangeable, so your cache key needs to include which send method produced the entry, not just which file it came from.
Handling the miss you didn’t expect
Because file_id isn’t guaranteed forever, your send path needs a fallback, not just a happy path. When you call sendVideo with a cached file_id and it comes back invalid, Telegram returns a Bad Request with an error description telling you the file reference is bad or the file itself wasn’t found. Catch that specific failure, fall back to uploading the source file fresh, and overwrite the stale cache entry with the new file_id. Without this fallback, a single expired entry turns into a broken send with no recovery, which is a worse failure mode than just uploading a bit more often than strictly necessary.
Don’t treat every failed send as a stale file_id though. Rate limiting, chat permission errors, and a user having blocked the bot all produce different error codes, and none of them are fixed by re-uploading. Match on the actual error description before you decide to fall back to a fresh upload, otherwise you’ll mask real problems by silently retrying uploads that were never going to help.
Where this actually saves you something
For a bot that sends the same handful of files repeatedly, an onboarding video, a pinned announcement, a set of stickers, the savings from caching file_id show up immediately. Instead of an upload on every send, it’s a lookup and a lightweight API call.
This matters more once your bot sits behind a proxy for its outbound connections to Telegram’s API, which is common when you’re managing hosting for several bots and want consistent, controllable egress. A re-upload pushes the full file through that proxy path every time. A file_id send doesn’t, because there’s no file body in the request, just a reference string. If you’re watching bandwidth or connection counts on the proxy side, the difference between a bot that caches file_id and one that doesn’t is visible in the traffic pattern, not just in theory.
It also matters for storage discipline on your own end. If your bot is re-reading a file from local disk and re-uploading it on every trigger, you’re doing that disk read and that upload for no reason once the file has already been sent once. Caching the file_id means the source file on disk only needs to exist long enough for the first upload to happen. After that, the reference is all your bot needs.
Keeping it simple
None of this requires a queue, a job runner, or anything elaborate. It’s a lookup table and a fallback branch in your send function. The part worth getting right is the key you cache on, source content plus send method plus bot identity, and the part worth not skipping is the fallback for when a cached file_id stops resolving. Get those two right and your bot stops paying an upload cost it already paid once.
If you’re setting up bot hosting or proxy routing for a Telegram project and want a second pair of eyes on how the pieces fit together, telegramvault.org is where we write about exactly this kind of infrastructure detail.
Get new guides and videos first — join the Telegram channel.