<!-- Markdown twin of https://pixelvault.dev/blog/host-images-from-discord -->

[← Back to blog](/blog)

By [PixelVault](/about) August 6, 2026

# Discord image links expire. Here's how to host them permanently.

You copied an image link out of Discord and dropped it into a website, a wiki, a forum post, or your bot's embed. It worked for a day. Now it's _"This content is no longer available."_ That's not a bug — Discord deliberately signs every attachment URL with an expiry, and any link used outside the app eventually dies. Here's why, and how to re-host those images on a URL that doesn't.

![A Discord message's expiring image attachment on the left being re-hosted into a stable PixelVault URL through a vault aperture on the right](https://img.pixelvault.dev/proj_mkoboesmx25d/img_wt53d18bzzos.jpg?w=1400&fmt=auto&q=auto)

This hero is hosted on PixelVault itself — a stable `img.pixelvault.dev` URL that won't expire the way the Discord attachment link it was built from would.

## Why Discord image links break

Open any image in Discord, copy its link, and you'll see something like `cdn.discordapp.com/attachments/…/file.png?ex=…&is=…&hm=…`. Those trailing parameters are a **signature and an expiry timestamp**. Discord started signing attachment URLs so they can only be served for a short window — the `ex` value is roughly 24 hours out. Inside the app you never notice, because Discord silently refreshes the link every time the message scrolls back into view. But a link you pasted somewhere else never gets refreshed — so once that window passes, the CDN rejects it and your image becomes a dead box.

This is by design, and it's not going to change: **Discord is a chat app, not an image host.** Its CDN is tuned for messages people are actively looking at, not for a logo embedded on your marketing site or a screenshot in a three-year-old forum thread. If your image needs to live anywhere other than inside Discord, a `discordapp.com` link is the wrong place to point at.

## Recovering a link that already expired

Be a little wary of tools promising to "fix" Discord links — the honest mechanics matter. A signed URL that has expired can be **refreshed, but only if the message still exists in Discord**: reopen the original message (or copy its link again) and Discord re-serves the still-live attachment with a fresh signature. If the original message was deleted, though, the bytes are gone from Discord's CDN and no tool can conjure them back. So the reliable move isn't to recover links after they die — it's to **re-host the image once, while it still loads**, onto a URL that won't expire on you in the first place.

## The fix: re-host on a permanent URL

The pattern is the same one that fixes expiring links everywhere — it's the identical fix for [Notion's one-hour signed image URLs](/blog/host-notion-api-images): fetch the image while Discord's link is valid, upload it once to storage you control, and use _that_ URL from then on. With PixelVault it's a single API call, and the URL you get back is permanent and immutable — no signature, no expiry, served from a global CDN with zero egress fees.

## If you're building a Discord bot

This is where it bites developers hardest. A bot that logs images, builds a gallery, generates art on command, or forwards attachments to a dashboard **cannot store a `discordapp.com` URL** — by the time anyone opens it, it's expired. The fix is to re-host the attachment the moment your bot sees it, then keep the permanent URL:

Re-host in a bot Use it anywhere

// Discord bot: re-host an attachment so the URL you post keeps working.  
import { Client, GatewayIntentBits } from "discord.js";  
  
// MessageContent is a privileged intent — needed to read attachments.  
const client = new Client({ intents: \[  
  GatewayIntentBits.Guilds,  
  GatewayIntentBits.GuildMessages,  
  GatewayIntentBits.MessageContent,  
\] });  
  
client.on("messageCreate", async (msg) => {  
  const att = msg.attachments.first();  
  if (!att) return;  
  
  // Pull the bytes while Discord's signed link is still valid…  
  const bytes = await fetch(att.url).then(r => r.blob());  
  
  // …and host them on a URL that isn't signed or time-boxed.  
  const form = new FormData();  
  form.append("file", bytes, att.name);  
  const res = await fetch("https://api.pixelvault.dev/v1/images", {  
    method: "POST",  
    headers: { Authorization: \`Bearer ${process.env.PIXELVAULT\_API\_KEY}\` },  
    body: form,  
  });  
  const { url } = (await res.json()).data;  
  // → a permanent https://img.pixelvault.dev/… URL, safe to store or embed.  
});  
  
client.login(process.env.DISCORD\_TOKEN);

Now the URL your bot stores in a database, posts into a webhook embed, or shows on a website keeps resolving long after the original Discord message has scrolled away or been deleted. If your bot _generates_ the image (an AI art command, a chart, a rendered card), host the generated bytes the same way; see [hosting AI-generated images](/blog/host-ai-generated-images) for that flow.

## Discord CDN vs a URL you own

Discord attachment link

PixelVault

Works outside the app

Expires (~24h signature)

Permanent

Safe to store in a DB

No — dead by next open

Yes

Survives message deletion

No

Yes — it's your copy

Resize / convert on the fly

Media proxy only, still expires

`?w=`, `fmt=auto`

Cost to serve at scale

n/a (breaks first)

Zero egress

## Free to start

PixelVault's free tier includes 200 MB storage, 500 uploads/month, and 1 GB bandwidth — no credit card, zero egress. Enough to back a bot or re-host the images on a small site before you pay anything, and paid plans start at [$9/month](/pricing). Start from the [API quickstart →](/docs), or if an AI agent is doing the work, hand it the [MCP server](/blog/mcp-image-hosting) or the [upload skill](/blog/host-images-with-your-coding-agent).
