All notes

IPv6 compression rules I keep forgetting

IPv6 addresses are easier to read once their text-compression rules become mechanical. Remove leading zeros within each group, compress one longest run of zero groups, prefer the leftmost run when tied and expand to exactly eight groups when checking an address.

about 19 minutes min read

The complete IPv6 address is:

2001:0db8:0000:0000:0000:ff00:0042:8329

The compressed form is:

2001:db8::ff00:42:8329

I understand both when they are placed beside each other.

The trouble begins when I meet only the compressed form and must remember:

  • how many groups disappeared
  • whether :: may appear twice
  • whether one zero group should be compressed
  • which zero run to choose
  • whether zeros may be removed from the right side of a group
  • where the port number begins

IPv6 text looks flexible because several textual forms can represent the same 128-bit value.

That flexibility is useful for input.

It is less useful for:

  • logs
  • searches
  • diagrams
  • configuration
  • audits
  • conversations between tired engineers

RFC 4291 defines the legal forms of IPv6 text representation. RFC 5952 adds a recommended canonical form so that systems generate one predictable spelling while continuing to accept other valid forms.

PARSING

accept every valid RFC 4291 spelling


GENERATING

use the RFC 5952 recommended spelling

This article begins with the ordinary colon-separated hexadecimal form. A valid IPv6 address may also end with a dotted-decimal IPv4 tail, which needs one additional counting rule later.

Compression changes how the address is written. It never changes the 128 bits underneath it.

When I forget a rule, I return to the full eight-group form and compress it again.

Begin with eight groups

In the ordinary hexadecimal form, an IPv6 address contains eight groups.

Each underlying group represents sixteen bits. In fully expanded form it contains exactly four hexadecimal digits; in ordinary legal text it may contain between one and four.

xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx

For example:

2001:0db8:0000:0000:0000:ff00:0042:8329

Count the groups:

1    2    3    4    5    6    7    8

2001:0db8:0000:0000:0000:ff00:0042:8329

The colon separates groups.

It does not separate bytes.

Each group contains:

4 hexadecimal digits
×
4 bits per digit
=
16 bits

Eight groups therefore contain:

8 × 16 = 128 bits

RFC 4291 describes the preferred text form as eight 16-bit pieces written with one to four hexadecimal digits each. The fully expanded form used here pads every piece to four digits so that all 128 bits are visible.

Whenever a compressed hexadecimal address becomes confusing, my first objective is:

Restore eight group-equivalents.

Once eight groups are visible, the address usually stops behaving like a riddle.

Rule one: remove leading zeros inside a group

Within each 16-bit group, leading zeros may be removed.

0db8 → db8

0042 → 42

0007 → 7

0000 → 0

Therefore:

2001:0db8:0000:0000:0000:ff00:0042:8329

becomes:

2001:db8:0:0:0:ff00:42:8329

RFC 5952 says leading zeros should be suppressed and that an individual all-zero group should be written as 0 when it is not part of the selected :: run. It also recommends lowercase hexadecimal letters.

The word leading matters.

Zeros may be removed only from the left side of one group.

Correct:

0042 → 42

These changes would alter the numeric value:

4200 → 42

1040 → 14

a000 → a

The zeros on the right are significant.

Each group is a hexadecimal number.

Removing a leading zero is like writing:

0042

as:

42

Removing a trailing zero would be like turning:

4200

into:

42

That is not abbreviation.

That is a different number wearing the first number’s coat.

Rule two: compress one run of zero groups

One consecutive run of all-zero 16-bit groups may be replaced with:

::

Starting with:

2001:db8:0:0:0:ff00:42:8329

the run:

0:0:0

can be compressed.

2001:db8::ff00:42:8329

The double colon represents the missing all-zero groups.

RFC 4291 allows :: to represent omitted zero groups and permits it at the beginning, middle or end of an address. It may appear only once because a second occurrence would make the number of omitted groups ambiguous.

This is valid:

2001:db8::42

This is not:

2001::db8::42

With two double colons, I cannot determine how many zero groups belong to each gap.

2001 : ? zeros : db8 : ? zeros : 42

The address has become a hexadecimal accordion with two unlabelled bellows.

One of them must remain closed.

Canonical form does not compress one zero group

RFC 4291’s general syntax permits flexible valid representations.

RFC 5952’s canonical recommendation is stricter.

It says :: should not be used merely to replace one zero group.

Start with:

2001:db8:0:1:2:3:4:5

The canonical form remains:

2001:db8:0:1:2:3:4:5

Not:

2001:db8::1:2:3:4:5

The second form can be interpreted as a valid 128-bit address because the omitted section must expand to one zero group.

It is not the recommended canonical spelling.

The practical rule I keep is:

Use :: for a run of
at least two zero groups.

That keeps :: useful as a genuine compression marker rather than an alternative way to write every isolated 0.

Compress the longest zero run

An address may contain more than one run of zero groups.

Only one can become ::.

RFC 5952 says to compress the longest run.

Consider:

2001:0:0:1:0:0:0:1

There are two candidate runs:

2001:[0:0]:1:[0:0:0]:1

The second run is longer.

The canonical form is:

2001:0:0:1::1

Not:

2001::1:0:0:0:1

The second spelling compresses only two groups while leaving a longer run visible.

It still describes the same address when expanded correctly.

It is not the preferred generated form.

The canonical rule is trying to produce:

  • the shortest practical representation
  • one stable representation
  • predictable search and comparison behaviour

Without that rule, two systems may continually rewrite the same address into different but valid strings.

When the runs tie, choose the leftmost

Consider:

2001:db8:0:0:1:0:0:1

It contains two equal runs:

2001:db8:[0:0]:1:[0:0]:1

Both runs contain two zero groups.

RFC 5952 says that when the longest runs are equal, the first run should be compressed.

The canonical form is:

2001:db8::1:0:0:1

Not:

2001:db8:0:0:1::1

My compressed rule set is therefore:

1. Find every consecutive run
   of all-zero groups.

2. Ignore isolated single zero groups.

3. Choose the longest run.

4. If lengths tie,
   choose the leftmost.

5. Replace that run with ::.

That is the part I keep forgetting because the address remains understandable whichever equal run I choose.

Canonical text is less concerned with whether I can decipher it.

It is concerned with every system choosing the same decipherable form.

Expansion is a counting exercise

Given:

2001:db8::ff00:42:8329

I count the visible groups.

Before :::

2001
db8

Two groups.

After :::

ff00
42
8329

Three groups.

Total visible groups:

2 + 3 = 5

An IPv6 address requires eight group-equivalents.

Therefore :: represents:

8 - 5 = 3 zero groups

The expanded address is:

2001:0db8:0000:0000:0000:ff00:0042:8329

Another example:

fd4a:91b2:7c30::25

Visible groups:

fd4a
91b2
7c30
25

Four groups.

Missing groups:

8 - 4 = 4

Expanded:

fd4a:91b2:7c30:0000:0000:0000:0000:0025

The complete validation rule is:

WITHOUT ::

exactly eight group-equivalents
must be present


WITH ::

fewer than eight group-equivalents
must be visible

missing zero groups
=
8 - visible group-equivalents

The phrase group-equivalents matters when a dotted-decimal IPv4 tail is present. In the ordinary hexadecimal form, each visible group counts as one.

:: must replace at least one all-zero group. It cannot represent nothing.

Therefore this is invalid:

1:2:3:4:5:6:7:8::

Eight groups are already visible, leaving no missing group for :: to restore.

The same count catches an address that is too short without compression:

2001:db8:1:2:3:4:5

Only seven groups are present and there is no ::.

This form is valid:

2001:db8::1:2:3:4:5

Seven groups are visible, so :: restores one zero group.

The formula works because :: appears only once and always represents one or more complete zero groups.

A dotted IPv4 tail counts as two groups

RFC 4291 also permits a mixed representation in which the low-order 32 bits are written as four decimal IPv4 octets.

For example:

::ffff:192.0.2.128

The dotted tail is not one 16-bit group.

It represents:

32 bits
=
two 16-bit group-equivalents

Count the visible contribution:

ffff
    = one group-equivalent

192.0.2.128
    = two group-equivalents

That gives:

1 + 2 = 3 visible group-equivalents

Therefore :: represents:

8 - 3 = 5 zero groups

The fully hexadecimal value is:

0000:0000:0000:0000:0000:ffff:c000:0280

RFC 5952 recommends mixed notation for recognised address types whose final 32 bits contain an embedded IPv4 address. When expanding or validating one of those forms, I count the dotted-decimal tail as two groups.

:: and ::1 are complete addresses

The unspecified IPv6 address contains eight zero groups.

0000:0000:0000:0000:0000:0000:0000:0000

Its compressed form is:

::

The loopback address contains seven zero groups followed by one:

0000:0000:0000:0000:0000:0000:0000:0001

Its compressed form is:

::1

Both forms are defined by the ordinary compression rules rather than being mysterious punctuation invented separately. RFC 4291 identifies :: as the unspecified address and ::1 as loopback.

For ::1, one group is visible.

1 visible group

Therefore :: represents:

8 - 1 = 7 zero groups

For ::, no groups are visible.

It represents all eight zero groups.

The rule still works at the extremes.

A prefix length is not another address group

An IPv6 prefix may be written as:

2001:db8:1234:5600::/56

The /56 says how many leading bits belong to the prefix.

It does not participate in the eight-group count.

Address:

2001:db8:1234:5600::


Prefix length:

56

To expand the address portion:

2001
db8
1234
5600

Four visible groups.

Therefore:

8 - 4 = 4 zero groups

Expanded:

2001:0db8:1234:5600:0000:0000:0000:0000/56

RFC 4291 defines IPv6 prefixes using ipv6-address/prefix-length, where the prefix length counts the leftmost contiguous bits. It also warns that leading zeros may be removed within a group, but trailing zeros must not be discarded because doing so changes the group’s value and therefore the prefix.

For example:

cd30

cannot be shortened to:

cd3

The first value ends in four zero bits.

The second does not.

Compression shortens text.

It must not quietly move the network boundary.

Lowercase is the canonical spelling

Hexadecimal accepts:

A B C D E F

and:

a b c d e f

They represent the same numeric values.

RFC 5952 recommends lowercase when generating canonical IPv6 text.

Therefore:

2001:DB8::ABCD

should normally be rendered as:

2001:db8::abcd

A parser should still accept legitimate uppercase input.

The output should be predictable.

This matters when addresses appear in:

  • logs
  • database keys
  • configuration diffs
  • search queries
  • inventory systems

Case should not create the illusion that two addresses differ.

Ideally, the application should parse the address into a binary address type and compare its numeric value rather than comparing raw input strings.

Brackets separate an address from its port

IPv4 can be written with a port as:

192.0.2.42:443

That notation becomes ambiguous with IPv6 because colons already separate address groups.

This:

2001:db8::42:443

could be interpreted entirely as an IPv6 address.

To combine an IPv6 literal with a port, place the address inside square brackets:

[2001:db8::42]:443

The brackets are not part of the IPv6 address.

They delimit the address inside a URI or host-and-port expression. RFC 3986 defines bracketed IP literals in URI host syntax, while RFC 5952 notes the need for an unambiguous delimiter when an IPv6 address is combined with a port number.

For example:

https://[2001:db8::42]:8443/

contains:

address:
    2001:db8::42

port:
    8443

The square brackets are tiny traffic cones placed around the colons so the port knows where to queue.

A zone identifier is not compression

A link-local address may appear as:

fe80::1234%eth0

The address portion is:

fe80::1234

The zone identifier is:

eth0

It tells the local system which interface or link context should be used for the scoped address.

It is not part of the 128-bit IPv6 address and does not affect zero compression.

Zone identifiers have local meaning and commonly correspond to an interface name or index. They can be relevant to link-local unicast addresses and scoped multicast addresses.

RFC 4007 defines IPv6 zones and zone indexes. RFC 9844, published in August 2025, describes how supporting user interfaces should accept addresses such as fe80::1%eth0 and notes that the zone identifier is meaningful only on the local node.

When expanding:

fe80::1234%eth0

I separate the zone first.

address:
    fe80::1234

zone:
    eth0

Then expand only the address:

fe80:0000:0000:0000:0000:0000:0000:1234

The zone remains:

%eth0

The %eth0 form is scoped-address input for operating-system tools and other interfaces that support zone identifiers. RFC 9844 does not establish a general syntax for placing a zone identifier into an HTTP URL fetched by a web browser.

Mixing the concepts makes the address appear to have grown a ninth, particularly unruly group.

It has not.

I let Python check my spelling

Python’s standard ipaddress module can show both compressed and expanded forms.

from ipaddress import IPv6Address

address = IPv6Address(
    "2001:0db8:0000:0000:0000:ff00:0042:8329"
)

print(address.compressed)
print(address.exploded)

The output is:

2001:db8::ff00:42:8329
2001:0db8:0000:0000:0000:ff00:0042:8329

The module exposes compressed and exploded representations for IPv6 addresses, making it useful for validating examples and normalising data before comparison.

This example uses the ordinary hexadecimal form. Mixed IPv4 notation and scoped addresses introduce additional display behaviour, so I test those formats separately when an application accepts them.

I use the tool after forming my own answer.

That gives me two useful signals.

My answer matches:

the rule is settling in.


My answer differs:

inspect the zero count,
longest run or tie-break.

The tool is not embarrassed when I forget whether the leftmost equal run wins.

It merely returns the canonical address with the calm patience of a librarian reshelving the same book.

My compact rule card

FULL ADDRESS

8 groups
4 hexadecimal digits per group
in fully expanded form
16 bits per group


LEADING ZEROS

Remove them inside each group.

0042 → 42
0000 → 0


ZERO COMPRESSION

Replace one consecutive run
of zero groups with ::.


CANONICAL CHOICE

Use a run of at least two groups.

Choose the longest run.

If tied, choose the leftmost.


FREQUENCY

:: may appear only once.

It must replace at least
one zero group.


GROUP COUNT

Without a double colon:
exactly eight group-equivalents.

With a double colon:
fewer than eight group-equivalents.

Missing groups
=
8 - visible group-equivalents


MIXED IPv4 FORM

A dotted-decimal IPv4 tail
represents the final 32 bits.

Count it as two 16-bit groups.


CASE

Use lowercase a-f.


PORT

[2001:db8::42]:443


ZONE

fe80::1%eth0

The zone is not part
of the 128-bit address.

That is the entire compression machine.

Questions I now ask

Groups

How many group-equivalents are visible?

Expansion

If :: appears, how many zero groups must it represent to reach eight?

Validity

Does :: replace at least one group, or are exactly eight groups present without it?

Mixed form

Is a dotted-decimal IPv4 tail being counted as two groups?

Leading zeros

Have zeros been removed only from the left of a group?

Zero run

Is :: replacing consecutive all-zero groups?

Frequency

Does :: appear no more than once?

Canonical form

Was the longest eligible zero run selected?

Tie

If two runs are equal, was the leftmost chosen?

Case

Are hexadecimal letters lowercase?

Prefix

Is /n being treated separately from the address?

Port

Is a literal IPv6 address enclosed in brackets before adding a port?

Zone

Is %eth0 or another zone identifier being treated as local interface context rather than address data?

These questions are quicker than staring at a compressed address and hoping the omitted zeros reveal themselves through atmosphere.

The mental model I am keeping

My earlier model was:

IPv6 compression

remove some zeros

replace some others with ::

try not to use too many colons

The stronger model is:

                       FULL IPv6 ADDRESS
                               │
                               ▼
                         EIGHT GROUPS
                               │
                  ┌────────────┴────────────┐
                  │                         │
                  ▼                         ▼
          REMOVE LEADING ZEROS      FIND ZERO-GROUP RUNS
                  │                         │
                  │                         ▼
                  │                 CHOOSE THE LONGEST
                  │                         │
                  │                 TIE → CHOOSE LEFTMOST
                  │                         │
                  └────────────┬────────────┘
                               ▼
                      REPLACE ONCE WITH ::
                               │
                               ▼
                    LOWERCASE HEXADECIMAL
                               │
                               ▼
                     CANONICAL TEXT FORM

Leading-zero removal shortens individual 16-bit groups.

:: shortens one selected run of complete zero groups.

Expansion restores enough group-equivalents to reach 128 bits.

Brackets, prefix lengths and zones belong to the surrounding notation, not the underlying address value.

IPv6 compression is not arbitrary once these rules are visible.

First shorten each group. Then shorten one run of groups. Never shorten the underlying value.

The full address contains eight 16-bit pieces.

Leading zeros may disappear from individual groups.

One longest sequence of all-zero groups may become ::.

Equal sequences send the prize to the leftmost contestant.

A dotted-decimal IPv4 tail accounts for the final two groups.

A port waits outside square brackets.

A zone identifier waits outside the address itself.

And whenever the colons begin multiplying in the dark, I expand the address back to eight groups and turn the lights on.

References and further reading