Looping Is Not in the GIF Spec

The GIF89a specification defines the signature, the color tables, the LZW encoder, the four interlacing passes, transparency, and the per-frame delay. It does not define animation looping. Download it and search: the word “loop” does not appear in the document once.

Every GIF you have ever watched repeat did so because of a 19-byte block that Netscape made up in 1995 and nobody ever standardized.

I saved the same eight frames twice, once with a loop count and once without:

loop.gif      1969 bytes
noloop.gif    1950 bytes

Nineteen bytes. That is the entire difference between an animation and a slideshow that stops.


The Nineteen Bytes

$ grep -abo NETSCAPE loop.gif
28:NETSCAPE
$ grep -abo NETSCAPE noloop.gif
$

At byte 25, three bytes before that match, the block starts:

0x21        extension introducer
0xFF        application extension label
0x0B        block size: 11
"NETSCAPE"  8 bytes
"2.0"       3 bytes
0x03        sub-block size: 3
0x01        sub-block ID: loop count
0x0000      loop count, little-endian; zero means forever
0x00        block terminator

The GIF89a spec does define a generic Application Extension, in section 26: a container where a vendor can stash eleven bytes of identifier and whatever payload they like, which decoders that don’t recognize the identifier must skip. That was the extension point Netscape used, and the feature won so completely that it is now the only reason the format still exists.

Section 14 of the same document, well before it gets there, tells you not to do this:

This approach is recommended in favor of using Application Extensions, which become overhead for all other applications that do not process them.

The spec authors saw the vendor-extension move coming, wrote down that it was a bad idea, and then shipped the mechanism anyway. Six years later a browser vendor used it to define the format’s most famous feature.

There is no RFC for this. There is no W3C note. There is no erratum to GIF89a. The WHATWG’s own wiki page for GIF does not document the block either; it links out to a third-party page that reverse-engineered it. Every GIF encoder in the world implements a format feature whose only description is somebody else’s notes.


The Header, and the Ghost of 1995

The first sixteen bytes tell you most of what the file is:

$ xxd -g 1 -l 32 loop.gif
00000000: 47 49 46 38 39 61 c8 00 64 00 81 00 00 ff 5a 28  GIF89a..d.....Z(
00000010: 14 14 1e 00 00 00 00 00 00 21 ff 0b 4e 45 54 53  .........!..NETS

GIF89a, then c8 00 and 64 00: 200 by 100, little-endian, which is the opposite of what JPEG, PNG, and MP4 all chose. Then a packed byte, 0x81, whose low three bits say the global color table holds 2^(1+1) colors. Four colors, twelve bytes, and then at byte 25 the Netscape block starts immediately.

The per-frame timing lives in a different block, the Graphic Control Extension, and it has a problem:

GCE at byte 44:  delay field = 8 (hundredths of a second)
GCE at byte 386: delay field = 8 (hundredths of a second)
8 Graphic Control Extensions total

Hundredths of a second. Two bytes, unsigned, little-endian. The finest interval the format can express is 10 milliseconds, so the theoretical ceiling is 100fps and there is no way to say 60fps evenly.

It gets worse in practice. Browsers treat a delay of 0 or 1 as 10, so asking for 100fps gets you 10fps. Firefox has done this since at least 2004, and the reason in the source comments is compatibility with how Netscape behaved. A delay of 2 is the practical floor, which puts the real maximum at 50fps.

So the animation feature was defined by Netscape, and the animation timing is still bounded by an emulation of Netscape’s bugs, thirty years after Netscape.


LZW Is Just Worse

Here is the part that makes GIF’s survival strange.

I took a 512x512 image with 151,112 distinct colors and reduced it to a 256-color adaptive palette. Then I saved those exact same quantized pixels two ways:

pal.gif      59198 bytes    (LZW)
pal.png      29154 bytes    (DEFLATE)
true.png     64106 bytes    (DEFLATE, all 151,112 colors, lossless)

Identical pixels, identical palette. PNG is less than half the size. And full-color lossless PNG, with every one of those 151,112 colors intact, is only 8% bigger than the GIF that threw 150,856 of them away.

LZW builds a dictionary of repeated index sequences with codes that grow from 2 bits to a hard ceiling of 12. DEFLATE does LZ77 plus Huffman coding and has no such ceiling. On anything that isn’t flat-color line art, it is not close.

To be fair to the format, GIF is not always the loser. My eight-frame animation of a flat orange circle on a flat dark background came out:

anim.gif     1969 bytes
anim.png     2170 bytes   (APNG)
anim.webp    4392 bytes

On a handful of frames of solid color, GIF’s per-frame overhead is small and LZW does fine. That is the shape of content GIF was designed for in 1987, when it was moving graphics over a 2400-baud modem, and it is still competitive there. It is just that nobody uses GIF for that anymore. They use it for compressed video of a person reacting to something, which is close to the worst possible input for a 256-color palette and a 12-bit dictionary.


The Format PNG Was Built To Replace

At the end of December 1994, CompuServe and Unisys jointly announced that software reading or writing GIF would need a license for US Patent 4,558,302, Terry Welch’s LZW patent, granted in 1985.

The response was immediate. PNG went from nothing to a frozen ninth draft on 7 March 1995, roughly ten weeks later. It is a better format on essentially every axis: better compression, 24-bit color, an alpha channel instead of one transparent index, and no patent. The League for Programming Freedom ran a “Burn All GIFs” campaign. Everyone agreed GIF should die.

The patent expired on 20 June 2003, and by then the argument had been over for years. PNG had won the still-image half completely.

It lost the other half because it did not have animation, and by the time APNG existed the web had already decided that a looping image was a GIF. The format that PNG was created to replace survives entirely on the one capability PNG shipped without, which the GIF specification also does not have, and which exists only because a browser vendor stuffed it into a generic extension slot and shipped it.


What To Do About It

  • Stop using GIF for video. A short MP4 or a WebM is smaller by an order of magnitude, plays with hardware decoding, and does not have a 256-color palette. Every major platform silently transcodes your GIF uploads to video already.
  • Use PNG for anything still. There is no case in 2026 where a static GIF is the right answer.
  • If you must ship GIF, quantize deliberately. Pick the palette yourself rather than letting the encoder guess, and dither on purpose. The default adaptive palette is rarely the best 256 colors for your image.
  • Never set a frame delay below 2. You will get 10fps instead of the 50 or 100 you asked for, and it will look like a bug in your code.
  • Check for the loop block if animations mysteriously play once. grep -abo NETSCAPE file.gif. Plenty of encoders omit it by default, including Pillow unless you pass loop=0.

Every format in this series has had a gap between what the specification says and what implementations do. GIF is the case where the gap swallowed the format. The spec describes a still-image format with optional frame timing. What the world actually uses is that spec plus one undocumented vendor block, and the vendor has been gone since 2003.

Sources

I’d appreciate a follow. You can subscribe with your email below. The emails go out once a week, or you can find me on Mastodon at @[email protected].

Programming web File-formats Compression Gif