--- a/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java
+++ b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java
@@ -56,6 +_,14 @@
 import java.util.concurrent.locks.LockSupport;
 import java.util.function.Predicate;
 
+// Folia start - region threading
+import io.papermc.paper.threadedregions.RegionizedServer;
+import io.papermc.paper.threadedregions.ThreadedRegionizer;
+import io.papermc.paper.threadedregions.TickRegionScheduler;
+import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap;
+import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet;
+// Folia end - region threading
+
 public final class ChunkHolderManager {
 
     private static final Logger LOGGER = LogUtils.getClassLogger();
@@ -78,29 +_,83 @@
     private final ConcurrentLong2ReferenceChainedHashTable<NewChunkHolder> chunkHolders = ConcurrentLong2ReferenceChainedHashTable.createWithCapacity(16384, 0.25f);
     private final ServerLevel world;
     private final ChunkTaskScheduler taskScheduler;
-    private long currentTick;
-
-    private final ArrayDeque<NewChunkHolder> pendingFullLoadUpdate = new ArrayDeque<>();
-    private final ObjectRBTreeSet<NewChunkHolder> autoSaveQueue = new ObjectRBTreeSet<>((final NewChunkHolder c1, final NewChunkHolder c2) -> {
-        if (c1 == c2) {
-            return 0;
-        }
-
-        final int saveTickCompare = Long.compare(c1.lastAutoSave, c2.lastAutoSave);
-
-        if (saveTickCompare != 0) {
-            return saveTickCompare;
-        }
-
-        final long coord1 = CoordinateUtils.getChunkKey(c1.chunkX, c1.chunkZ);
-        final long coord2 = CoordinateUtils.getChunkKey(c2.chunkX, c2.chunkZ);
-
-        if (coord1 == coord2) {
-            throw new IllegalStateException("Duplicate chunkholder in auto save queue");
-        }
-
-        return Long.compare(coord1, coord2);
-    });
+    // Folia start - region threading
+    public static final class HolderManagerRegionData {
+        private final ArrayDeque<NewChunkHolder> pendingFullLoadUpdate = new ArrayDeque<>();
+        private final ObjectRBTreeSet<NewChunkHolder> autoSaveQueue = new ObjectRBTreeSet<>((final NewChunkHolder c1, final NewChunkHolder c2) -> {
+            if (c1 == c2) {
+                return 0;
+            }
+
+            final int saveTickCompare = Long.compare(c1.lastAutoSave, c2.lastAutoSave);
+
+            if (saveTickCompare != 0) {
+                return saveTickCompare;
+            }
+
+            final long coord1 = CoordinateUtils.getChunkKey(c1.chunkX, c1.chunkZ);
+            final long coord2 = CoordinateUtils.getChunkKey(c2.chunkX, c2.chunkZ);
+
+            if (coord1 == coord2) {
+                throw new IllegalStateException("Duplicate chunkholder in auto save queue");
+            }
+
+            return Long.compare(coord1, coord2);
+        });
+
+        public void merge(final HolderManagerRegionData into, final long tickOffset) {
+            // Order doesn't really matter for the pending full update...
+            into.pendingFullLoadUpdate.addAll(this.pendingFullLoadUpdate);
+
+            // We need to copy the set to iterate over, because modifying the field used in compareTo while iterating
+            // will destroy the result from compareTo (However, the set is not destroyed _after_ iteration because a constant
+            // addition to every entry will not affect compareTo).
+            for (final NewChunkHolder holder : new ArrayList<>(this.autoSaveQueue)) {
+                holder.lastAutoSave += tickOffset;
+                into.autoSaveQueue.add(holder);
+            }
+        }
+
+        public void split(final int chunkToRegionShift, final Long2ReferenceOpenHashMap<HolderManagerRegionData> regionToData,
+                          final ReferenceOpenHashSet<HolderManagerRegionData> dataSet) {
+            for (final NewChunkHolder fullLoadUpdate : this.pendingFullLoadUpdate) {
+                final int regionCoordinateX = fullLoadUpdate.chunkX >> chunkToRegionShift;
+                final int regionCoordinateZ = fullLoadUpdate.chunkZ >> chunkToRegionShift;
+
+                final HolderManagerRegionData data = regionToData.get(CoordinateUtils.getChunkKey(regionCoordinateX, regionCoordinateZ));
+                if (data != null) {
+                    data.pendingFullLoadUpdate.add(fullLoadUpdate);
+                } // else: fullLoadUpdate is an unloaded chunk holder
+            }
+
+            for (final NewChunkHolder autoSave : this.autoSaveQueue) {
+                final int regionCoordinateX = autoSave.chunkX >> chunkToRegionShift;
+                final int regionCoordinateZ = autoSave.chunkZ >> chunkToRegionShift;
+
+                final HolderManagerRegionData data = regionToData.get(CoordinateUtils.getChunkKey(regionCoordinateX, regionCoordinateZ));
+                if (data != null) {
+                    data.autoSaveQueue.add(autoSave);
+                } // else: autoSave is an unloaded chunk holder
+            }
+        }
+    }
+
+    private ChunkHolderManager.HolderManagerRegionData getCurrentRegionData() {
+        final ThreadedRegionizer.ThreadedRegion<io.papermc.paper.threadedregions.TickRegions.TickRegionData, io.papermc.paper.threadedregions.TickRegions.TickRegionSectionData> region =
+            TickRegionScheduler.getCurrentRegion();
+
+        if (region == null) {
+            return null;
+        }
+
+        if (this.world != null && this.world != region.getData().world) {
+            throw new IllegalStateException("World check failed: expected world: " + this.world.getWorld().getKey() + ", region world: " + region.getData().world.getWorld().getKey());
+        }
+
+        return region.getData().getHolderManagerRegionData();
+    }
+    // Folia end - region threading
+
 
     public ChunkHolderManager(final ServerLevel world, final ChunkTaskScheduler taskScheduler) {
         this.world = world;
@@ -185,8 +_,13 @@
     }
 
     public void close(final boolean save, final boolean halt) {
+        // Folia start - region threading
+        this.close(save, halt, true, true, true);
+    }
+    public void close(final boolean save, final boolean halt, final boolean first, final boolean last, final boolean checkRegions) {
+        // Folia end - region threading
         TickThread.ensureTickThread("Closing world off-main");
-        if (halt) {
+        if (first && halt) { // Folia - region threading
             LOGGER.info("Waiting 60s for chunk system to halt for world '" + WorldUtil.getWorldName(this.world) + "'");
             if (!this.taskScheduler.halt(true, TimeUnit.SECONDS.toNanos(60L))) {
                 LOGGER.warn("Failed to halt generation/loading tasks for world '" + WorldUtil.getWorldName(this.world) + "'");
@@ -196,9 +_,10 @@
         }
 
         if (save) {
-            this.saveAllChunks(true, true, true);
+            this.saveAllChunksRegionised(true, true, true, first, last, checkRegions); // Folia - region threading
         }
 
+        if (last) { // Folia - region threading
         MoonriseRegionFileIO.flush(this.world);
 
         if (halt) {
@@ -220,28 +_,35 @@
         }
 
         this.taskScheduler.setShutdown(true);
+        } // Folia - region threading
     }
 
     void ensureInAutosave(final NewChunkHolder holder) {
-        if (!this.autoSaveQueue.contains(holder)) {
-            holder.lastAutoSave = this.currentTick;
-            this.autoSaveQueue.add(holder);
+        // Folia start - region threading
+        final HolderManagerRegionData regionData = this.getCurrentRegionData();
+        if (!regionData.autoSaveQueue.contains(holder)) {
+            holder.lastAutoSave = RegionizedServer.getCurrentTick();
+            regionData.autoSaveQueue.add(holder);
+            // Folia end - region threading
         }
     }
 
     public void autoSave() {
         final List<NewChunkHolder> reschedule = new ArrayList<>();
-        final long currentTick = this.currentTick;
+        final long currentTick = RegionizedServer.getCurrentTick(); // Folia - region threading
         final long maxSaveTime = currentTick - Math.max(1L, PlatformHooks.get().configAutoSaveInterval(this.world));
         final int maxToSave = PlatformHooks.get().configMaxAutoSavePerTick(this.world);
-        for (int autoSaved = 0; autoSaved < maxToSave && !this.autoSaveQueue.isEmpty();) {
-            final NewChunkHolder holder = this.autoSaveQueue.first();
+        // Folia start - region threading
+        final HolderManagerRegionData regionData = this.getCurrentRegionData();
+        for (int autoSaved = 0; autoSaved < maxToSave && !regionData.autoSaveQueue.isEmpty();) {
+            final NewChunkHolder holder = regionData.autoSaveQueue.first();
+            // Folia end - region threading
 
             if (holder.lastAutoSave > maxSaveTime) {
                 break;
             }
 
-            this.autoSaveQueue.remove(holder);
+            regionData.autoSaveQueue.remove(holder); // Folia - region threading
 
             holder.lastAutoSave = currentTick;
             if (holder.save(false) != null) {
@@ -255,15 +_,38 @@
 
         for (final NewChunkHolder holder : reschedule) {
             if (holder.getChunkStatus().isOrAfter(FullChunkStatus.FULL)) {
-                this.autoSaveQueue.add(holder);
+                regionData.autoSaveQueue.add(holder); // Folia start - region threading
             }
         }
     }
 
     public void saveAllChunks(final boolean flush, final boolean shutdown, final boolean logProgress) {
-        final List<NewChunkHolder> holders = this.getChunkHolders();
-
-        if (logProgress) {
+        // Folia start - region threading
+        this.saveAllChunksRegionised(flush, shutdown, logProgress, true, true, true);
+    }
+    public void saveAllChunksRegionised(final boolean flush, final boolean shutdown, final boolean logProgress, final boolean first, final boolean last, final boolean checkRegion) {
+        final List<NewChunkHolder> holders = new java.util.ArrayList<>(this.chunkHolders.size() / 10);
+        // we could iterate through all chunk holders with thread checks, however for many regions the iteration cost alone
+        // will multiply. to avoid this, we can simply iterate through all owned sections
+        final int regionShift = this.world.moonrise$getRegionChunkShift();
+        final int width = 1 << regionShift;
+        for (final LongIterator iterator = io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegion().getOwnedSectionsUnsynchronised(); iterator.hasNext();) {
+            final long sectionKey = iterator.nextLong();
+            final int offsetX = CoordinateUtils.getChunkX(sectionKey) << regionShift;
+            final int offsetZ = CoordinateUtils.getChunkZ(sectionKey) << regionShift;
+
+            for (int dz = 0; dz < width; ++dz) {
+                for (int dx = 0; dx < width; ++dx) {
+                    final NewChunkHolder holder = this.getChunkHolder(offsetX | dx, offsetZ | dz);
+                    if (holder != null) {
+                        holders.add(holder);
+                    }
+                }
+            }
+        }
+        // Folia end - region threading
+
+        if (first && logProgress) { // Folia - region threading
             LOGGER.info("Saving all chunkholders for world '" + WorldUtil.getWorldName(this.world) + "'");
         }
 
@@ -292,6 +_,12 @@
         }
         for (int i = 0, len = holders.size(); i < len; ++i) {
             final NewChunkHolder holder = holders.get(i);
+            // Folia start - region threading
+            if (!checkRegion && !TickThread.isTickThreadFor(this.world, holder.chunkX, holder.chunkZ)) {
+                // skip holders that would fail the thread check
+                continue;
+            }
+            // Folia end - region threading
             try {
                 final NewChunkHolder.SaveStat saveStat = holder.save(shutdown);
                 if (saveStat != null) {
@@ -327,7 +_,7 @@
                 }
             }
         }
-        if (flush) {
+        if (last && flush) { // Folia - region threading
             MoonriseRegionFileIO.flush(this.world);
             try {
                 MoonriseRegionFileIO.flushRegionStorages(this.world);
@@ -732,7 +_,13 @@
     }
 
     public void tick() {
-        ++this.currentTick;
+        // Folia start - region threading
+        final ThreadedRegionizer.ThreadedRegion<io.papermc.paper.threadedregions.TickRegions.TickRegionData, io.papermc.paper.threadedregions.TickRegions.TickRegionSectionData> region =
+            TickRegionScheduler.getCurrentRegion();
+        if (region == null) {
+            throw new IllegalStateException("Not running tick() while on a region");
+        }
+        // Folia end - region threading
 
         final int sectionShift = ((ChunkSystemServerLevel)this.world).moonrise$getRegionChunkShift();
 
@@ -746,7 +_,7 @@
             return removeDelay <= 0L;
         };
 
-        for (final PrimitiveIterator.OfLong iterator = this.sectionToChunkToExpireCount.keyIterator(); iterator.hasNext();) {
+        for (final LongIterator iterator = region.getOwnedSectionsUnsynchronised(); iterator.hasNext();) {
             final long sectionKey = iterator.nextLong();
 
             if (!this.sectionToChunkToExpireCount.containsKey(sectionKey)) {
@@ -1031,26 +_,56 @@
         if (changedFullStatus.isEmpty()) {
             return;
         }
-        if (!TickThread.isTickThread()) {
-            this.taskScheduler.scheduleChunkTask(() -> {
-                final ArrayDeque<NewChunkHolder> pendingFullLoadUpdate = ChunkHolderManager.this.pendingFullLoadUpdate;
-                for (int i = 0, len = changedFullStatus.size(); i < len; ++i) {
-                    pendingFullLoadUpdate.add(changedFullStatus.get(i));
-                }
-
-                ChunkHolderManager.this.processPendingFullUpdate();
-            }, Priority.HIGHEST);
-        } else {
-            final ArrayDeque<NewChunkHolder> pendingFullLoadUpdate = this.pendingFullLoadUpdate;
-            for (int i = 0, len = changedFullStatus.size(); i < len; ++i) {
-                pendingFullLoadUpdate.add(changedFullStatus.get(i));
-            }
-        }
+
+        // Folia start - region threading
+        final Long2ObjectOpenHashMap<List<NewChunkHolder>> sectionToUpdates = new Long2ObjectOpenHashMap<>();
+        final List<NewChunkHolder> thisRegionHolders = new ArrayList<>();
+
+        final int regionShift = this.world.moonrise$getRegionChunkShift();
+        final ThreadedRegionizer.ThreadedRegion<io.papermc.paper.threadedregions.TickRegions.TickRegionData, io.papermc.paper.threadedregions.TickRegions.TickRegionSectionData> thisRegion
+            = TickRegionScheduler.getCurrentRegion();
+
+        for (final NewChunkHolder holder : changedFullStatus) {
+            final int regionX = holder.chunkX >> regionShift;
+            final int regionZ = holder.chunkZ >> regionShift;
+            final long holderSectionKey = CoordinateUtils.getChunkKey(regionX, regionZ);
+
+            // region may be null
+            if (thisRegion != null && this.world.regioniser.getRegionAtUnsynchronised(holder.chunkX, holder.chunkZ) == thisRegion) {
+                thisRegionHolders.add(holder);
+            } else {
+                sectionToUpdates.computeIfAbsent(holderSectionKey, (final long keyInMap) -> {
+                    return new ArrayList<>();
+                }).add(holder);
+            }
+        }
+        if (!thisRegionHolders.isEmpty()) {
+            thisRegion.getData().getHolderManagerRegionData().pendingFullLoadUpdate.addAll(thisRegionHolders);
+        }
+
+        if (!sectionToUpdates.isEmpty()) {
+            for (final Iterator<Long2ObjectMap.Entry<List<NewChunkHolder>>> iterator = sectionToUpdates.long2ObjectEntrySet().fastIterator();
+                iterator.hasNext();) {
+                final Long2ObjectMap.Entry<List<NewChunkHolder>> entry = iterator.next();
+                final long sectionKey = entry.getLongKey();
+
+                final int chunkX = CoordinateUtils.getChunkX(sectionKey) << regionShift;
+                final int chunkZ = CoordinateUtils.getChunkZ(sectionKey) << regionShift;
+
+                final List<NewChunkHolder> regionHolders = entry.getValue();
+                this.taskScheduler.scheduleChunkTaskEventually(chunkX, chunkZ, () -> {
+                    ChunkHolderManager.this.getCurrentRegionData().pendingFullLoadUpdate.addAll(regionHolders);
+                    ChunkHolderManager.this.processPendingFullUpdate();
+                }, Priority.HIGHEST);
+
+            }
+        }
+        // Folia end - region threading
     }
 
     private void removeChunkHolder(final NewChunkHolder holder) {
         holder.onUnload();
-        this.autoSaveQueue.remove(holder);
+        this.getCurrentRegionData().autoSaveQueue.remove(holder); // Folia - region threading
         PlatformHooks.get().onChunkHolderDelete(this.world, holder.vanillaChunkHolder);
         this.chunkHolders.remove(CoordinateUtils.getChunkKey(holder.chunkX, holder.chunkZ));
     }
@@ -1063,7 +_,7 @@
             throw new IllegalStateException("Cannot unload chunks recursively");
         }
         final int sectionShift = this.unloadQueue.coordinateShift; // sectionShift <= lock shift
-        final List<ChunkUnloadQueue.SectionToUnload> unloadSectionsForRegion = this.unloadQueue.retrieveForAllRegions();
+        final List<ChunkUnloadQueue.SectionToUnload> unloadSectionsForRegion = this.unloadQueue.retrieveForCurrentRegion(); // Folia - threaded regions
         int unloadCountTentative = 0;
         for (final ChunkUnloadQueue.SectionToUnload sectionRef : unloadSectionsForRegion) {
             final ChunkUnloadQueue.UnloadSection section
@@ -1381,7 +_,13 @@
 
     // only call on tick thread
     private boolean processPendingFullUpdate() {
-        final ArrayDeque<NewChunkHolder> pendingFullLoadUpdate = this.pendingFullLoadUpdate;
+        // Folia start - region threading
+        final HolderManagerRegionData data = this.getCurrentRegionData();
+        if (data == null) {
+            return false;
+        }
+        final ArrayDeque<NewChunkHolder> pendingFullLoadUpdate = data.pendingFullLoadUpdate;
+        // Folia end - region threading
 
         boolean ret = false;
 
@@ -1392,9 +_,7 @@
             ret |= holder.handleFullStatusChange(changedFullStatus);
 
             if (!changedFullStatus.isEmpty()) {
-                for (int i = 0, len = changedFullStatus.size(); i < len; ++i) {
-                    pendingFullLoadUpdate.add(changedFullStatus.get(i));
-                }
+                this.addChangedStatuses(changedFullStatus); // Folia - region threading
                 changedFullStatus.clear();
             }
         }
