Computing Decoded

Same Minecraft Seed, Different World: I Compared 190 Million Blocks

A Minecraft seed is 8 bytes, yet an empty world takes 28 MB. I regenerated a world and compared every block, with the code to test your own.

Free PDF guide9 pages · diagrams, code, glossary

Free, 9 pages. The email is optional: leave it empty and the PDF downloads anyway. If you add it, I send new guides and nothing else. Privacy

Preview of the PDF guide: Same Minecraft Seed, Different World: I Compared 190 Million Blocks

I typed one number and got a whole world. That number, the seed, fits in 8 bytes. The world, with nobody ever playing in it, took 28.4 MB. So I opened the files, measured every piece, and tested the deal everyone repeats: same seed, same world. It came back 150,124 blocks different.

NOT AN OFFICIAL MINECRAFT PRODUCT. NOT APPROVED BY OR ASSOCIATED WITH MOJANG OR MICROSOFT. All measurements come from Mojang’s own server, version 26.3, seed 20261103, with no player. All diagrams here are our own.

Inside the file

A world on disk is a folder of region files, r.X.Z.mca. Each one holds 32 x 32 chunks. Its first 4,096 bytes are a table with 1,024 entries, and each entry says where one chunk starts and how many 4 KiB sectors it takes. To find a chunk the game computes x + 32 * z and reads that entry: an array index, no search. For the chunk at (5, 3), entry 101 in my file reads 00 01 9c 02: sector 412, length 2 sectors.

Where 28 MB goes

Part Bytes
1,936 finished chunks 12,285,209
2,420 unfinished chunks around them 2,634,517
Headers and empty space in sectors 13,465,554
Region files in total 28,385,280

The finished chunks hold 190,316,544 blocks. At 2 bytes each that would be 380.6 MB, so the real figure is 31 times smaller, without losing anything. Three ideas do it:

  • Uniform sections. 62.1% of 46,464 sections hold a single block type, so the game writes that block once.
  • Palettes. A busier section lists its block types, 11.5 on average, and each block becomes a small index.
  • zlib. It shrinks chunk data 6.9 times more on its own.

You pay for disorder

I changed real chunks and measured the stored bytes:

Change Blocks changed Stored bytes
A small hollow house 258 16 to 293 more, in three runs
The whole chunk to stone 86,917 5,311 to 654
The whole chunk to 16 random block types 97,508 6,525 to 56,298

Random indexes have nothing for zlib to exploit: the block data alone is 49,152 bytes, and through zlib it becomes 49,173. Saving a stone chunk right after the fill gave 2,623 bytes, because 505 pending block updates are stored too.

Is the world infinite?

No. The default border is 29,999,984 blocks from the centre, which gives about 14 trillion chunks. At 6,346 bytes per finished chunk, that is about 89 petabytes. This is an estimate from chunks near the start of one world, so treat it as an order of magnitude.

The experiment

Same seed, same version, same server, same order of generation. I compared all blocks of the 1,936 chunks finished in both worlds.

Comparison Blocks different Chunks changed
World against itself 0 0
Second run 177,281 668
Second run, clock frozen with tick freeze 150,124 490
Reverse order, clock frozen 453,063 1,495
Same order, clock frozen, one reported core 144,101 508

To trust the tool I ran two controls. A world against itself gives exactly 0. A hollow house of 258 blocks, built with the clock frozen, shows up as exactly 258 in its chunk. I also wrote the comparison twice, separately, and both give identical numbers.

Top-down maps of the same forest in two runs of the same seed, and the same map with the differences in amber. Drawn from the real block data in our own colours.

These maps are drawn from the block data in our own colours, not game textures: run 1, run 2, and run 1 with every column whose top block differs in amber.

Most of the world matched: 99.92% of the blocks. 71% of the differences are trees. Half of the chunks with many trees changed, against 6.9% of the chunks with none. In reverse order the rock changes too: 12,995 blocks of andesite became diorite.

What explains it, and what does not

The Minecraft Wiki says features are written in a later pass, can place blocks in the surrounding 3 x 3 chunks, and can spill into chunks already decorated. So where chunks meet, order matters, and the reverse-order run confirms that it does.

What I could not explain is why the same order still differs. I suspected worker threads racing, so I started the server with -XX:ActiveProcessorCount=1, and it still differed by 144,101 blocks. That flag limits what Java reports, so it does not prove a single thread ran, but it does rule out core count as the whole answer. Mojang’s tracker has this as MC-55596, opened in 2014, confirmed and reopened.

Test your own world

Save this as compare.mjs. It needs only Node. Close the game first, and compare two copies of a world folder.

// Compare two Minecraft Java worlds block by block. No dependencies, Node 18 or newer.
// Usage: node compare.mjs <world-a>/region <world-b>/region
// Newer versions keep the overworld in dimensions/minecraft/overworld/region.
import {readdirSync, readFileSync} from 'node:fs';
import zlib from 'node:zlib';

// Minimal NBT reader: chunks are big-endian tagged trees.
function readNbt(b) {
  let p = 3 + b.readUInt16BE(1); // skip the root tag and its name
  const take = (n) => { const v = b.subarray(p, p + n); p += n; return v; };
  const read = {
    1: () => take(1).readInt8(), 2: () => take(2).readInt16BE(), 3: () => take(4).readInt32BE(),
    4: () => take(8).readBigInt64BE(), 5: () => take(4).readFloatBE(), 6: () => take(8).readDoubleBE(),
    7: () => take(read[3]()), 8: () => take(take(2).readUInt16BE()).toString('utf8'),
    9: () => { const t = read[1](), n = read[3](), out = []; for (let i = 0; i < n; i++) out.push(read[t]()); return out; },
    10: () => { const o = {}; for (;;) { const t = read[1](); if (t === 0) return o; o[read[8]()] = read[t](); } },
    11: () => { const n = read[3](), out = []; for (let i = 0; i < n; i++) out.push(read[3]()); return out; },
    12: () => take(read[3]() * 8),
  };
  return read[10]();
}

// A block state as one comparable string: name plus its sorted properties.
// Version 26.3 mixes three shapes in one palette: "minecraft:stone", {"": "minecraft:stone"} and {id, properties}. Older versions use {Name, Properties}.
const stateName = (s) => {
  if (typeof s === 'string') return s + '[]';
  const props = s.Properties ?? s.properties ?? {};
  return (s.Name ?? s.id ?? s['']) + JSON.stringify(Object.keys(props).sort().map((k) => [k, props[k]]));
};

// One section's 4,096 blocks. Indexes never cross a 64-bit long, lowest bits first.
function blocksOf(section) {
  const bs = section.block_states;
  if (!bs) return null;
  const names = bs.palette.map(stateName);
  if (names.length === 1) return new Array(4096).fill(names[0]);
  const bits = Math.max(4, Math.ceil(Math.log2(names.length))), perLong = Math.floor(64 / bits);
  const mask = (1n << BigInt(bits)) - 1n, out = new Array(4096);
  for (let i = 0; i < 4096; i++) {
    const long = bs.data.readBigUInt64BE(Math.floor(i / perLong) * 8);
    out[i] = names[Number((long >> BigInt((i % perLong) * bits)) & mask)];
  }
  return out;
}

// Every finished chunk of a region folder, keyed by its position.
function chunksOf(dir) {
  const chunks = new Map();
  for (const file of readdirSync(dir).filter((f) => f.endsWith('.mca'))) {
    const buf = readFileSync(`${dir}/${file}`);
    for (let i = 0; i < 1024; i++) { // the header: entry i is the chunk x + 32 * z of this region
      const offset = buf.readUIntBE(i * 4, 3) * 4096;
      if (!offset || buf[i * 4 + 3] === 0) continue;
      const length = buf.readInt32BE(offset);
      if (buf[offset + 4] !== 2) continue; // 2 = zlib, the default
      const nbt = readNbt(zlib.inflateSync(buf.subarray(offset + 5, offset + 4 + length)));
      if (String(nbt.Status ?? nbt.status).endsWith('full')) chunks.set(`${nbt.xPos},${nbt.zPos}`, nbt);
    }
  }
  return chunks;
}

const [dirA, dirB] = process.argv.slice(2), A = chunksOf(dirA), B = chunksOf(dirB);
let blocks = 0, different = 0, chunksCompared = 0, chunksChanged = 0;
for (const [key, a] of A) {
  const b = B.get(key);
  if (!b) continue;
  chunksCompared++;
  const sectionsB = new Map(b.sections.map((s) => [s.Y, s]));
  let here = 0;
  for (const s of a.sections) {
    const x = blocksOf(s), y = sectionsB.has(s.Y) ? blocksOf(sectionsB.get(s.Y)) : null;
    if (!x || !y) continue;
    blocks += 4096;
    for (let i = 0; i < 4096; i++) if (x[i] !== y[i]) here++;
  }
  different += here;
  if (here) chunksChanged++;
}
console.log({chunksCompared, blocks, different, chunksChanged});
node compare.mjs world-a/region world-b/region

To make two comparable worlds, use the same seed and version, load the same area, and on a server run tick freeze first so nothing simulates.

What it means

A seed is a world, plus the exact code that reads it, plus the order things happened to run in. Your save is not a cache of something the game could recalculate: it is the only record of how your world came out. If a save gets too big, trimming chunks you never visited is reasonable. If you care about a place, keep the file.

Glossary

Seed
The 64-bit number a world generator starts from.
Chunk
A column of the world, 16 x 16 blocks wide and 384 tall in the overworld, saved as one unit.
Section
A 16 x 16 x 16 cube of a chunk. A chunk has 24 of them.
Palette
The short list of block types one section uses. Each block stores a small number pointing into it.
Region file
One .mca file holding 32 x 32 chunks, with a table of 1,024 entries at its start.
Sector
A 4 KiB piece of a region file. Every chunk takes a whole number of sectors.
Feature
Something placed after the land is shaped, such as a tree or an ore vein. It can write into the surrounding chunks.
tick freeze
A server command that stops the game clock: no random ticks, weather or falling blocks.

Practice

Try these before opening the answers.

Which entry of the region table holds the chunk at x = 5 and z = 3?

5 + 32 x 3 = 101.

A section uses 16 block types. How many bytes do the block indexes take?

16 types need 4 bits. 4,096 x 4 / 8 = 2,048 bytes, plus the palette.

Why can zlib not shrink a section of uniformly random block indexes?

Random data has no repeats to exploit. In my test 49,152 bytes became 49,173 bytes through zlib alone.

What result would have shown that the differences come from simulation, not generation?

Two frozen-clock worlds coming back identical. They did not: they differed by 150,124 blocks.

Frequently asked questions

Does the same Minecraft seed always give the same world?

Nearly, but not exactly. In my test 99.92% of the blocks matched, with the same seed, version and order. Coastlines and mountains came back the same, while trees and some rock did not. Mojang's bug tracker lists this as MC-55596.

Why does an empty Minecraft world take space at all?

The game saves the terrain it generates, not only what players change. Chunks near a loaded area are stored, finished or not, and each one is rounded up to 4 KiB sectors.

Can I delete chunks and regenerate them from the seed?

You can, and it saves space. But they will not come back exactly as they were, even on the same version, and an update can change them more. Keep the file for places you care about.

Is the Minecraft world infinite?

No. The default world border is 29,999,984 blocks from the centre. That is about 14 trillion chunks, which I estimate at about 89 petabytes if all were saved at the size I measured.

Do these numbers come from Minecraft itself?

Yes. They come from Mojang's own server, version 26.3, with no player. The video's background footage comes from VoxeLibre and Mineclonia, two different free games, and it is labelled that way.

Keep this guide: free PDF

9 pages with the diagrams, the code, a glossary and practice questions, ready to print or save.

Free, 9 pages. The email is optional: leave it empty and the PDF downloads anyway. If you add it, I send new guides and nothing else. Privacy

New guides, straight to your inbox

One email when a new video and guide come out. No spam, no tracking pixels.

More guides