When you export an animated emote you have three levers to make the file smaller: fewer colours, fewer frames, or a smaller canvas. Platforms fix the canvas for you (Twitch wants 112/56/28 px, Discord wants 128 px), so in practice it comes down to colours versus frames. I wanted to know which one is worth reaching for first, so I measured it.
The limits
- Discord custom emoji: under 256 KB, static or animated.
- Twitch animated emote, manual upload: three GIFs at 112, 56 and 28 px, up to 512 KB each, at most 60 frames. Auto-resize upload: one square file up to 1 MB.
Both come from the platforms' own help pages; check them before you rely on my numbers, they change.
The ladder I use
The encoder gets a byte budget and walks down a fixed ladder until the file fits:
- 256 colours, all frames
- 128 colours
- 64 colours
- 128 colours, every other frame dropped (delay doubled so the loop keeps its speed)
- 64 colours, every other frame dropped
- 32 colours, every other frame dropped
If nothing fits, the smallest attempt is returned and flagged as over budget, so the UI can say so instead of silently shipping a file that will be rejected. Each result also records what it gave up ("64 colors", "every other frame dropped"), which is what lets you show the trade-off to the user.
Two details that matter more than they look:
- One shared palette for all frames, built from opaque pixels only, with the transparent slot reserved as the last index. Per-frame palettes tend to flicker and cost more bytes.
- Binarise alpha first. GIF has 1-bit transparency, so half-transparent edge pixels have to be decided up front, otherwise they quantise into odd fringe colours.
The core of it, with gifenc:
const ATTEMPTS = [ { colors: 256, drop: false }, { colors: 128, drop: false }, { colors: 64, drop: false }, { colors: 128, drop: true }, { colors: 64, drop: true }, { colors: 32, drop: true }, ]; for (const a of ATTEMPTS) { const datas = a.drop ? frames.filter((_, i) => i % 2 === 0) : frames; const delay = a.drop ? baseDelay * 2 : baseDelay; const bytes = encodeGif(datas, size, delay, a.colors); // shared palette, alpha binarised if (bytes.length <= maxBytes) return { bytes, tradeoffs: describe(a) }; keepSmallest(bytes); } return smallestAttemptFlaggedOverBudget(); What it actually saves
I re-implemented the same ladder in Python/Pillow (not the production encoder, so absolute sizes will differ a little) and ran it on eight 8-frame, 120 ms-per-frame emote loops (a cartoon corgi doing Hi, GG, LOL, Hype, Love, Sad, Sip, Sleep), downscaled to 128 px with a transparent background. Sizes in KB, range across the eight files:
| Step | Size |
|---|---|
| 256 colours, 8 frames | 46–52 KB |
| 128 colours | 40–46 KB |
| 64 colours | 33–38 KB |
| 128 colours, 4 frames | 20–23 KB |
| 64 colours, 4 frames | 16–19 KB |
| 32 colours, 4 frames | 11–15 KB |
Takeaways from this small sample:
- Dropping every other frame roughly halves the file. One step, about 40–50% off.
- Halving the palette only buys about 10–15% per step. 256 to 64 colours saved roughly a quarter of the size.
- For a short loop like this, the 256 KB limit isn't even close: the first rung already passes with ~50 KB. The ladder earns its keep on long source clips. AI video tools and screen recordings hand you 60–120 frames, and there frame count is the lever that decides whether you land under 256 KB.
So my rule is: if the file is over budget, count the frames first. Trim colours only for the last few KB, because it is the lever that visibly costs you quality (banding on gradients and soft shadows) while saving the least.
Caveats
- Eight files from one character is a small sample. The ratios should hold for similar flat-colour cartoon art; photographic or heavily gradient content compresses differently.
- Dropping frames changes the feel of fast motion. Doubling the delay keeps the loop duration the same, but a 4-frame hype loop is choppier than an 8-frame one. Preview it at 28 px, which is how most viewers see it.
- Twitch also caps animated emotes at 60 frames and asks that they not flash more than three times per second. Those are separate checks from file size.
Why I care
I build a tool that turns one character image into a matching set of animated Twitch and Discord emotes, and the export step has to produce files that pass these limits every time. If you want to see the output, or just need to check or resize your own GIFs, there is a free in-browser checker and resizer at https://animatedemotemaker.com (no sign-up for the free tools). The tool uses AI to generate the animations; this article was drafted with AI assistance and the measurements above were run on real files.
