Tessellate is a Fabric and NeoForge performance mod for Minecraft 1.21.1. It groups distant loaded areas into independent regions and lets those regions run on different CPU cores. If one region gets too expensive, Tessellate can slow it down without dragging the rest of the server with it.
Support and discussion: join the Tessellate Discord.
Download Tessellate from CurseForge.
| Situation | What Tessellate does |
|---|---|
| Several distant bases are busy | Runs their regions concurrently |
| One region exceeds the server budget | Reduces only that region's simulation rate |
| Two regions become close enough to interact | Merges them before they tick |
| A worker reaches unsafe shared state | Falls back to serial ticking for the session |
This works best when players or forced-loaded areas are spread out. One dense base, mob farm, or machine cluster is still one region, so Tessellate cannot split that work across several cores. It also doesn't replace single-thread optimizers such as Lithium; the two solve different problems.
For the current benchmark, we ran an ABBA sequence and restarted the server before every arm. The
test used four forced-loaded regions 2,048 blocks apart, each with exactly 1,200 persistent zombies
(4,800 total). Both modes used adaptive throttling and the same nine-mod setup. The only setting we
changed was parallelTicking.
xychart-beta
title "Median global tick time"
x-axis ["Serial", "Parallel"]
y-axis "MSPT - lower is better" 0 --> 45
bar [40.55, 15.75]
Parallel ticking cut median MSPT by 61.2% and p95 MSPT by 62.5%. All four regions held 20 TPS in both parallel runs. In the serial runs, the adaptive budget had to slow some regions because their work could only run one after another.
| Mode, median of two arms | Median MSPT | Median p95 | Regions at 20 TPS | Slowest region |
|---|---|---|---|---|
| Serial regional ticking | 40.55 ms | 50.80 ms | 0/4 and 1/4 | 10.0-10.6 TPS |
| Parallel regional ticking | 15.75 ms | 19.05 ms | 4/4 in both arms | 20.0 TPS |
Raw arms and benchmark controls
| Arm | Median MSPT | Median p95 | Regions at 20 TPS | Slowest region |
|---|---|---|---|---|
| Serial A | 40.1 ms | 50.8 ms | 0/4 | 10.0 TPS |
| Parallel A | 15.1 ms | 18.8 ms | 4/4 | 20.0 TPS |
| Parallel B | 16.4 ms | 19.3 ms | 4/4 | 20.0 TPS |
| Serial B | 41.0 ms | 50.8 ms | 1/4 | 10.6 TPS |
- Hardware: Intel Core Ultra 7 255H, 16 cores, Java 21.
- Runtime: Minecraft 1.21.1, NeoForge 21.1.248, Lithium 0.15.4, C2ME 0.4.0-alpha.0.120, Mekanism and Mekanism Generators 10.7.19.85, Naturalist 2.0.3, Citadel 2.7.1, Friends & Foes 4.0.27, Resourceful Lib 3.0.12, and ScalableLux 0.3.0-alpha.0.8.
- Each arm discarded two 8-second warmup windows, then used the median of five 8-second samples.
asyncRegionLoops=falsein both arms sotick queryincluded the worker critical path. Shipping async mode does not join workers every tick, so its main-thread MSPT is not directly comparable.ItemEntity.Age, a vanilla field outside Tessellate, measured each region's effective TPS.- Every arm verified exact entity count, separate regions, correct execution mode, drained boundary queues, and zero boundary failures. Parallel arms also required worker overlap of at least two.
Reproduce the same serial/parallel sequence with:
bash tools/ab_parallel.sh 4 1200This benchmark is deliberately built around several separate regions. It is not a promise of the same speedup on every server. One hot region still runs on one core, and different mixes of mobs, machines, and mods will behave differently.
flowchart LR
L[Loaded chunks] --> M[Region map]
M --> A[Region A worker]
M --> B[Region B worker]
M --> C[Region C worker]
A --> Q[Main-thread global commits]
B --> Q
C --> Q
Q --> N[Packets, lifecycle, saves]
Each worker handles the local simulation for its region. Anything that touches shared Minecraft or mod state is queued and applied on the main thread once the region is safe.
Tessellate divides the loaded world into sections of 4x4 chunks. Nearby entity-ticking sections are joined into a region, and that whole region ticks as one unit.
With the default settings, regions merge when they come within 2 sections of each other. Two separate regions are therefore at least 3 sections (192 blocks) apart, far enough that one cannot reach the other during a single tick.
The map updates as chunks load and unload. Run /tessellate regions to see it.
Tessellate measures the cost of each region every tick. If the server is about to miss its tick-time target, the most expensive region gets a tick divisor. A divisor of 4 runs that region at 5 TPS while unaffected regions can stay at 20.
Running the whole region once every fourth tick would produce the right average rate, but it feels awful in play:
| gated (all work every 4th tick) | sliced (1/4 of the work every tick) | |
|---|---|---|
| bystander's TPS | 19.6 | 20.0 |
| MSPT, mean | 20.5 ms | 23.7 ms |
| MSPT, 95th percentile | 210.9 ms | 32.9 ms |
That gated version caused a 200 ms hitch every fourth tick, which made everyone stutter. Tessellate instead runs a quarter of the region's entities on every tick. Each entity still averages 5 TPS, but the work is spread out.
Entity IDs and block positions determine slice membership, so the same mob or hopper stays in the same slice each cycle.
The throttle looks at the whole tick before deciding whether to step in. Chunk work, networking,
and block entities outside regions all consume time that regions cannot control, so the available
region budget is whatever remains under targetTickMillis. If the server is comfortably holding
20 TPS, nothing is throttled.
Separate regions cannot interact during a tick, which makes it possible to run them on different threads. The harder part is keeping every shared boundary safe:
- Entity storage is sharded by grid cell. Vanilla keeps every entity section in one map plus one balanced tree. Sharding by cell rather than by region matters: a section key's coordinates are chunk coordinates, so the shard is a pure function of the key, and regions merging or splitting migrates nothing.
- Entity lifecycle callbacks run on the main thread. One entity crossing a chunk section updates six level-global containers: entity tracking, the player list, mob navigation, dragon parts, game-event listeners, and the mod event bus. Workers queue those and the main thread replays them, which replaces six concurrency problems with one rule.
- Scheduled ticks are region-owned. Each region has an ordinary vanilla
LevelTickschild; chunk tick containers move between children at quiescent topology changes. Owner-local writes stay on the worker and cross-owner writes are handed off. - Block events are region-owned. Each region has an ordered, deduplicating queue. Its block callbacks run on the owner worker; only successful event packets and positional sound/network effects are committed on the main thread.
Most simulation stays on the region worker. Shared work crosses one of these measured boundaries:
| boundary | region-worker work | main-thread commit or global barrier |
|---|---|---|
| scheduled block/fluid ticks | drain the region-owned vanilla LevelTicks child |
cross-owner scheduling is handed to the destination owner |
| chunk/random ticks | tick with worker-local vanilla random state | chunk/player broadcasts and packet delivery |
| entity ticks | core entity simulation | add/remove lifecycle, persistent callbacks, and entity insertion |
| teleports/dimension changes | request the transition | replay the transition after the source owner is idle |
| block-entity ticks | core block-entity simulation | ticker and fresh block-entity registration |
| block events | ordered owner callback queue | successful packets and positional sound/network effects |
| natural spawning | independent searches with one snapshot and atomic cap reservations | entity insertion and vanilla spawn-state replay |
| custom spawners/mod callbacks | none | arbitrary global mod code remains main-threaded |
| commands | none | quiesce all owners, drain handoffs, then execute the whole command |
| autosave/chunk unload | none while the owner is active | busy autosaves retry and direct saves lease the owner; optional C2ME can serialize unload snapshots on its workers |
| explicit saves and shutdown | none | quiesce and drain before taking the global snapshot or closing storage |
| topology merge/split | none while owners are active | wait affected owners, drain against old ownership, then replace the map |
/tessellate phases reports queued, replayed, direct, pending, elapsed main-thread time, failures,
balance, and the last source region for each boundary. The counters are bounded by boundary type;
they do not retain an ever-growing region history.
With asyncRegionLoops=true, a busy region keeps its ownership claim and only misses its own next
slot. Other regions and the main server tick continue instead of waiting at a global barrier.
Targeted leases protect packets, commands, chunk topology, entity loading and unloading, and saves.
Tessellate runs this ownership path without C2ME and has no build, metadata, or class dependency on it.
Vanilla prepares a safe unload snapshot on the main thread and writes it asynchronously. If C2ME is
installed, it can also run ChunkSerializer.write for that snapshot on its workers. Loaded
autosaves are still serialized during an idle main-thread slice; they do not wait on a busy region.
| Check | Result |
|---|---|
| JVM regression suite | 81 tests passed |
| Tessellate-only NeoForge GameTests | All 5 required tests passed without external mods |
| Scheduled ticks | 30 minutes, 422 rebuild cycles, worker peak 5, no failure or queued work left |
| Block events | 30 minutes, 1,001,984 callbacks, worker peak 4, packets stayed on main |
| Natural spawning | Three parallel runs, worker overlap confirmed, global and local caps held |
| Deferred writes | 6,947 level writes and 2,592 entity callbacks replayed exactly |
| Full-mod save/unload | 1,200 entities survived a 49-chunk unload/reload exactly |
| Main-thread boundaries | 8,519 deferred operations balanced with zero pending work |
These checks cover the workloads listed here, not every possible mod interaction. The serial fallback is still necessary because no test suite can prove that arbitrary mod code is thread-safe.
Throttling has an intentional cost: when a region exceeds its share, everything inside it runs slower. Mobs move more slowly, farms produce less, and hoppers move fewer items. That is useful for containing a lag machine, but Tessellate cannot tell a lag machine from a legitimately busy base. It will slow either one.
If you would rather let the whole server drop below 20 TPS than slow a player's build, set
regions.adaptiveThrottling = false. Region tracking stays on and you keep the /tessellate regions
diagnostics.
config/tessellate-common.toml.
| option | default | what it does |
|---|---|---|
regions.enabled |
true |
region tracking; off makes the mod inert |
regions.adaptiveThrottling |
true |
the regional TPS system |
regions.budgetMillis |
25.0 |
minimum budget retained for region work |
regions.targetTickMillis |
45.0 |
tick time to steer toward; raise to intervene later |
regions.maxTickDivisor |
16 |
slowest a region may be driven (1.25 TPS) |
regions.minThrottleMillis |
2.0 |
regions cheaper than this are never slowed |
regions.sectionShift |
2 |
region grid granularity, 4x4 chunks |
regions.parallelTicking |
true |
tick regions on worker threads |
regions.directWorkerChunkReads |
true |
resolve loaded worker chunk reads before synchronous-load instrumentation |
regions.parallelNaturalSpawning |
true |
run owner-region spawn searches concurrently with level-wide cap reservations |
regions.shardEntityStorage |
true |
storage isolation required by parallelTicking |
regions.asyncRegionLoops |
true |
independent per-region loops without a per-tick barrier |
regions.scopedScheduledTicks |
true |
region-own block/fluid schedulers; false uses one serial fallback child |
regions.scopedBlockEvents |
true |
region-own block-event callbacks; false uses the serial fallback queue |
compatibility.mainThreadEntities |
["creaturefeature:toadstool"] |
entity types forced onto the main thread until their mods adopt the compatibility API |
compatibility.forceSerialMods |
[] |
loaded mod IDs that force serial region ticking for the session |
compatibility.rulesEndpoint |
Tessellate's public rules table | curated compatibility restrictions; blank disables remote rules |
compatibility.rulesApiKey |
public key | read-only rules key; never use a service-role key |
compatibility.reportEndpoint |
Tessellate's report service | sends structured failure reports; blank opts out |
compatibility.reportApiKey |
public key | report-service key; blank opts out; never use a service-role key |
To return to serial regional ticking while diagnosing a mod conflict:
parallelTicking = false
asyncRegionLoops = falseRuntime fallbacks always log a suspected mod and source frame locally. Reporting is enabled by
default and only sends a report when Tessellate encounters a compatibility failure. Set both
compatibility.reportEndpoint and compatibility.reportApiKey to "" to opt out. Reports contain the
loader/game/mod versions, failure class, failing entity or block-entity type when known, one suspected
frame, and the loaded mod inventory; they do not contain raw logs, server addresses, world names, or
player data. Direct client writes to the report and rule tables are denied; ingestion is limited to
60 reports per source address and 5,000 reports project-wide per hour. Reports remain untrusted and
must be reviewed before a maintainer promotes a version-scoped compatibility rule.
The reporting service is fail-open. If it is unavailable, Minecraft continues starting and running;
the failed upload is only logged locally. To self-host the service, run
supabase/compatibility_reports.sql, deploy the tessellate-report Edge Function, and replace the
report endpoint and public key with your project's values.
The same SQL creates maintainer-owned entity and mod compatibility rule tables. At startup, Tessellate
applies only rules matching a loaded mod, loader, and exact version (or *). Remote rules can force an
entity or block entity onto the main thread, serialize entity ticks, disable parallel natural spawning,
or force serial region ticking. They can only reduce concurrency; a remote rule never enables a local
feature. Set rulesEndpoint = "" to disable remote rules. Reporting and rule downloads can be
disabled independently, and submitted reports never create rules. Promote a confirmed entity report
with SQL like:
insert into public.tessellate_entity_compatibility_rules
(mod_id, mod_version, loader, entity_type_id, reason)
values ('example_mod', '1.2.3', 'neoforge', 'example_mod:unsafe_entity',
'entity tick uses main-thread-only state');The tessellate_mod_compatibility_rules.action values are main_thread_block_entity,
serialize_entity_ticks, disable_parallel_spawning, and force_serial_regions. Only the block-entity
action uses target_id; the other actions use an empty string.
In Supabase, open tessellate_entity_compatibility_candidates or
tessellate_mod_compatibility_candidates. They group repeated failures, show unreviewed, active, or
disabled, and include ready-to-run promotion_sql. Candidate views remain maintainer-only; the public
rules key cannot read failure reports or candidate details.
Use compatibility.forceSerialMods only when the failure cannot be isolated to an entity type.
/tessellate regions: region map, per-region cost and tick rate, execution mode/tessellate phases: worker/main calls, time, overlap, lock wait, failures and deferred queue depth/tessellate violations: thread-ownership violations recorded (should be empty)/tessellate visualize: stable region map plus GPU-rendered perimeter curtains;T#is the pooled worker that most recently ran the region
When Lithium is installed, Tessellate disables Lithium's alloc.chunk_random,
entity.inactive_navigations, and experimental entity.block_caching mixins through Lithium's
supported mod override metadata. They keep mutable state shared by independent region threads;
Tessellate supplies worker-local random state and an atomic path-type cache, while entity block
caching falls back to Lithium's regular collision path.
Mods whose entity tick touches main-thread-only state can opt that entity type out of region workers during mod initialization:
TessellateApi.registerMainThreadEntity(MyEntityTypes.MY_ENTITY.get());Tessellate will tick those entities on the server thread. Registration is process-wide and idempotent; register only the affected types so other entities retain parallel ticking. Tessellate waits for all active regions before replaying a whole entity tick because its interaction range is unknown. Block entities have the same safety fallback:
TessellateApi.registerMainThreadBlockEntity(MyBlockEntityTypes.MACHINE.get());Treat whole-type registration as a safety fallback, not the normal compatibility fix. Most mods can
keep mob ticks parallel by fixing their shared state and isolating only the unsafe operation. A
ConcurrentHashMap can solve independent map-access races, but changing the collection alone does
not make compound read/modify/write sequences, iteration plus mutation, or invariants spanning
several objects thread-safe.
Prefer clear ownership, atomic operations, a lock around the full invariant, immutable snapshots,
or the appropriate executeOnRegion/executeOnMainThread boundary. Use
registerMainThreadEntity only when the entire tick truly cannot be made safe; most mobs should not
need it after the underlying code is corrected.
If only one operation is unsafe, keep the entity tick parallel and hand off just that operation:
TessellateApi.executeOnMainThread(() -> updateMainThreadOnlyState());Calls are always queued and return before the operation runs, so code after this call cannot depend on its result.
World access should stay on the region that owns its position. Owner-local calls run immediately; cross-region and external-thread calls are queued for the current owner:
TessellateApi.executeOnRegion(level, machinePos, () -> {
BlockEntity blockEntity = level.getBlockEntity(machinePos);
if (blockEntity instanceof MyMachine machine) {
machine.acceptTransfer(amount);
}
});Use TessellateApi.ownsCurrentRegion(level, pos) to select an owner-local fast path, or
requireCurrentRegion(level, pos) as a development assertion. isRegionThread() reports whether
the current call has a Tessellate region ownership scope; it does not mistake an unrelated async
thread for the server thread.
Main-thread code that touches a known world position should use the positional overload. It leases the target owner before running while unrelated regions continue:
TessellateApi.executeOnMainThread(level, machinePos,
() -> updateMainThreadOnlyWorldState(level, machinePos));We tested independent region loops with:
- NeoForge alone, no other mods
- Lithium 0.15.4 + Mekanism 10.7.19.85 + Mekanism Generators 10.7.19.85 + Naturalist 2.0.3 + Citadel 2.7.1 + Friends & Foes 4.0.27 + Resourceful Lib 3.0.12 + ScalableLux 0.3.0-alpha.0.8
- C2ME 0.4.0-alpha.0.120 with the full mod set above
Across these setups, entity queries returned byte-identical results with sharding on and off (0 differences across 25 spatial queries). Block entities and scheduled ticks behaved the same, with no ownership violations or serial fallbacks. Vanilla and Naturalist retention probes passed, all six tested Generators blocks created block entities, and two real clients negotiated the optional visualizer payload while three regions ran concurrently.
Lithium needs a little special handling because its spawning optimization reads entity storage
internals directly. Tessellate's sharded storage preserves that path. Region block entities can also
register Lithium world-border listeners concurrently, so Tessellate serializes that one registration
method instead of locking whole ticks around Lithium's internal WeakHashMap.
Tessellate protects the shared state it knows about, but it cannot make unknown mod code thread-safe.
- Other mods can still introduce races. Tessellate protects four level-global containers reachable from entity ticks. A mod that keeps its own unsafe global state can still race.
- C2ME 0.4.0-alpha.0.120 has one test limitation. Full live benchmarks, save/unload, restart, and shutdown pass with C2ME installed. Its natural-spawning GameTest fails in both serial and parallel regional modes, so that specific test is not counted as passing compatibility coverage.
- Numbers above are from a synthetic benchmark on one machine. Your server is not this benchmark.
./gradlew build # requires JDK 21
./gradlew runServer # dev server with RCON on 25585
python tools/bench_isolation.py --mobs 6000
python tools/bench_parallel.py --areas 4 --mobs 1200
python tools/verify_blockevents.py --soak-seconds 1800
The benchmark scripts read the RCON password from TESSELLATE_RCON_PASSWORD; use the same value for
rcon.password in the dev server's local server.properties.