Benchmark

The merge tolerance that decides whether an STL reads as watertight

Why do two tools disagree about whether the same STL is watertight?

Measured 2026-09-08

Result

STL stores every triangle as three independent vertices, so a 2,562 vertex icosphere is written out as 15,360 loose vertices forming 5,120 separate shells. Reading it back means guessing which of those coincide. When the coordinates are bit-identical, exact matching recovers all 2,562 and the mesh is watertight. Add vertex noise of 1e-06 mm and exact matching recovers almost nothing: it takes a merge tolerance of 0.1 mm, about 2.9 percent of the 3.459 mm shortest edge, before the mesh closes again. Two tools reporting differently on one file are usually not disagreeing about the file. They are using different tolerances.

The same sphere before and after an STL round trip

Property Original meshSTL, read with no merging
Vertices 2,56215,360
Faces 5,1205,120
Boundary edges 015,360
Non-manifold edges 00
Shells 15,120
Watertight YesNo
Volume (cubic mm) 522,467.37undefined

Nothing was damaged in transit. The face count is identical and every coordinate is intact. What the format dropped is the knowledge that three triangles were sharing one corner, and without it the mesh has no interior and therefore no volume.

Merge tolerance sweep, clean coordinates

Tolerance (mm)VerticesBoundary edgesShellsWatertightMatches original
02,56201YesYes
1 x 10^-72,56201YesYes
1 x 10^-52,56201YesYes
1 x 10^-42,56201YesYes
0.0012,56201YesYes
0.012,56201YesYes
0.12,56201YesYes
12,56201YesYes

With coordinates that survived export unchanged, every tolerance including exact matching gets the original mesh back. This is the case that makes vertex welding look like a solved problem.

Merge tolerance sweep, vertex jitter of 1e-06 mm

Tolerance (mm)VerticesBoundary edgesShellsWatertightMatches original
015,36015,3605,120NoNo
1 x 10^-715,35715,3605,120NoNo
1 x 10^-54,2897,0681,201NoNo
1 x 10^-42,8221,36214NoNo
0.0012,5961743NoNo
0.012,5821041NoNo
0.12,56201YesYes
12,56201YesYes

A millionth of a millimetre is far below anything that could matter physically, and it is enough to break exact matching completely: 15,360 vertices and 5,120 shells at zero tolerance. The mesh does not close until 0.1 mm, four orders of magnitude above the error being corrected.

Merge tolerance sweep, vertex jitter of 0.01 mm

Tolerance (mm)VerticesBoundary edgesShellsWatertightMatches original
015,36015,3605,120NoNo
1 x 10^-715,36015,3605,120NoNo
1 x 10^-515,36015,3605,120NoNo
1 x 10^-415,36015,3605,120NoNo
0.00115,35915,3605,120NoNo
0.0114,63715,3545,117NoNo
0.14,5377,7461,472NoNo
12,6695925NoNo

At this noise level no tolerance in the sweep recovers the original. Even 1 mm, a third of the shortest edge, leaves 592 boundary edges and 5 shells. Past a certain scatter the answer is not a better tolerance.

The same sphere written to indexed formats

FormatVerticesBoundary edgesShellsWatertightVolume (cubic mm)
PLY2,56201Yes522,467.37
OBJ2,56201Yes522,467.37
GLB2,56201Yes522,467.37

PLY, OBJ and GLB store a vertex list and index into it, so the sharing is written down rather than inferred. All three come back at 2,562 vertices and one watertight shell with no tolerance involved anywhere.

Why the tolerance has to be read against the model, not in absolute terms

A tolerance of 0.1 mm is 2.9 percent of this model's shortest edge, which is why it closes the mesh without destroying detail. The same 0.1 mm applied to a model whose shortest edge is 0.05 mm would weld genuinely distinct corners together. This is why a tolerance that works well on one file collapses another, and why a tool cannot simply pick a good default and be done.

How this was measured

An icosphere, 4 subdivisions, scaled to 100 mm across: 2,562 vertices, 5,120 faces, watertight, volume 522,467.37 cubic mm, shortest edge 3.459 mm. It was exported to STL and read back at eight merge tolerances from exact to 1 mm, under four levels of vertex jitter, and also written to PLY, OBJ and GLB for comparison.

Software and versions
trimesh 5.1.0, numpy 2.5.2
Date measured
2026-09-08
Script
scripts/measure-weld-tolerance.py
Raw data
weld-tolerance-2026-09-08.json

This data is published under CC BY 4.0. Reuse the numbers freely, with a link back so a reader can check the method.

What this does not show

Stated so the result is not read wider than it goes.

  • what any named repair tool reports, since that needs those tools
  • Jitter here is synthetic and uniform. Real coordinate error from an exporter is neither, and tends to cluster where the exporter did arithmetic.
  • The sweep stops at 1 mm. Larger tolerances would eventually weld the model into something smaller than itself, which is a different failure worth its own measurement.

Where this is used

MeshLab , Blender