diff --git a/paper-server/nms-patches/ChunkProviderServer.patch b/paper-server/nms-patches/ChunkProviderServer.patch
index adb0b48483..1309bf5263 100644
--- a/paper-server/nms-patches/ChunkProviderServer.patch
+++ b/paper-server/nms-patches/ChunkProviderServer.patch
@@ -83,7 +83,7 @@
      public void tick(BooleanSupplier booleansupplier) {
          this.world.getMethodProfiler().enter("purge");
          this.chunkMapDistance.purgeTickets();
-@@ -318,13 +346,13 @@
+@@ -318,13 +346,19 @@
          this.lastTickTime = i;
          WorldData worlddata = this.world.getWorldData();
          boolean flag = worlddata.getType() == WorldType.DEBUG_ALL_BLOCK_STATES;
@@ -95,32 +95,43 @@
              int k = this.world.getGameRules().getInt(GameRules.RANDOM_TICK_SPEED);
              BlockPosition blockposition = this.world.getSpawn();
 -            boolean flag2 = worlddata.getTime() % 400L == 0L;
-+            boolean flag2 = world.ticksPerAnimalSpawns != 0L && worlddata.getTime() % world.ticksPerAnimalSpawns == 0L; // CraftBukkit // PAIL: TODO monster ticks
++            // CraftBukkit start - Other mob type spawn tick rate
++            boolean spawnAnimalThisTick = world.ticksPerAnimalSpawns != 0L && worlddata.getTime() % world.ticksPerAnimalSpawns == 0L;
++            boolean spawnMonsterThisTick = world.ticksPerMonsterSpawns != 0L && worlddata.getTime() % world.ticksPerMonsterSpawns == 0L;
++            boolean spawnWaterThisTick = world.ticksPerWaterSpawns != 0L && worlddata.getTime() % world.ticksPerWaterSpawns == 0L;
++            boolean spawnAmbientThisTick = world.ticksPerAmbientSpawns != 0L && worlddata.getTime() % world.ticksPerAmbientSpawns == 0L;
++            boolean flag2 = spawnAnimalThisTick;
++            // CraftBukkit end
  
              this.world.getMethodProfiler().enter("naturalSpawnCount");
              int l = this.chunkMapDistance.b();
-@@ -353,8 +381,30 @@
+@@ -353,8 +387,35 @@
                              for (int j1 = 0; j1 < i1; ++j1) {
                                  EnumCreatureType enumcreaturetype = aenumcreaturetype1[j1];
  
 +                                // CraftBukkit start - Use per-world spawn limits
++                                boolean spawnThisTick = true;
 +                                int limit = enumcreaturetype.b();
 +                                switch (enumcreaturetype) {
 +                                    case MONSTER:
++                                        spawnThisTick = spawnMonsterThisTick;
 +                                        limit = world.getWorld().getMonsterSpawnLimit();
 +                                        break;
 +                                    case CREATURE:
++                                        spawnThisTick = spawnAnimalThisTick;
 +                                        limit = world.getWorld().getAnimalSpawnLimit();
 +                                        break;
 +                                    case WATER_CREATURE:
++                                        spawnThisTick = spawnWaterThisTick;
 +                                        limit = world.getWorld().getWaterAnimalSpawnLimit();
 +                                        break;
 +                                    case AMBIENT:
++                                        spawnThisTick = spawnAmbientThisTick;
 +                                        limit = world.getWorld().getAmbientSpawnLimit();
 +                                        break;
 +                                }
 +
-+                                if (limit == 0) {
++                                if (!spawnThisTick || limit == 0) {
 +                                    continue;
 +                                }
 +                                // CraftBukkit end
@@ -131,7 +142,7 @@
  
                                      if (object2intmap.getInt(enumcreaturetype) <= k1) {
                                          SpawnerCreature.a(enumcreaturetype, this.world, chunk, blockposition);
-@@ -507,12 +557,18 @@
+@@ -507,12 +568,18 @@
  
          @Override
          protected boolean executeNext() {
diff --git a/paper-server/nms-patches/World.patch b/paper-server/nms-patches/World.patch
index fa0ca962d3..459078da7d 100644
--- a/paper-server/nms-patches/World.patch
+++ b/paper-server/nms-patches/World.patch
@@ -18,7 +18,7 @@
  public abstract class World implements GeneratorAccess, AutoCloseable {
  
      protected static final Logger LOGGER = LogManager.getLogger();
-@@ -40,7 +51,39 @@
+@@ -40,7 +51,43 @@
      private final WorldBorder worldBorder;
      private final BiomeManager biomeManager;
  
@@ -36,6 +36,8 @@
 +    public List<EntityItem> captureDrops;
 +    public long ticksPerAnimalSpawns;
 +    public long ticksPerMonsterSpawns;
++    public long ticksPerWaterSpawns;
++    public long ticksPerAmbientSpawns;
 +    public boolean populating;
 +
 +    public CraftWorld getWorld() {
@@ -55,11 +57,13 @@
 +        this.world = new CraftWorld((WorldServer) this, gen, env);
 +        this.ticksPerAnimalSpawns = this.getServer().getTicksPerAnimalSpawns(); // CraftBukkit
 +        this.ticksPerMonsterSpawns = this.getServer().getTicksPerMonsterSpawns(); // CraftBukkit
++        this.ticksPerWaterSpawns = this.getServer().getTicksPerWaterSpawns(); // CraftBukkit
++        this.ticksPerAmbientSpawns = this.getServer().getTicksPerAmbientSpawns(); // CraftBukkit
 +        // CraftBukkit end
          this.methodProfiler = gameprofilerfiller;
          this.worldData = worlddata;
          this.worldProvider = dimensionmanager.getWorldProvider(this);
-@@ -49,6 +92,35 @@
+@@ -49,6 +96,35 @@
          this.worldBorder = this.worldProvider.getWorldBorder();
          this.serverThread = Thread.currentThread();
          this.biomeManager = new BiomeManager(this, flag ? worlddata.getSeed() : WorldData.c(worlddata.getSeed()), dimensionmanager.getGenLayerZoomer());
@@ -95,7 +99,7 @@
      }
  
      @Override
-@@ -105,6 +177,17 @@
+@@ -105,6 +181,17 @@
  
      @Override
      public boolean setTypeAndData(BlockPosition blockposition, IBlockData iblockdata, int i) {
@@ -113,7 +117,7 @@
          if (isOutsideWorld(blockposition)) {
              return false;
          } else if (!this.isClientSide && this.worldData.getType() == WorldType.DEBUG_ALL_BLOCK_STATES) {
-@@ -112,9 +195,22 @@
+@@ -112,9 +199,22 @@
          } else {
              Chunk chunk = this.getChunkAtWorldCoords(blockposition);
              Block block = iblockdata.getBlock();
@@ -137,7 +141,7 @@
                  return false;
              } else {
                  IBlockData iblockdata2 = this.getType(blockposition);
-@@ -125,6 +221,7 @@
+@@ -125,6 +225,7 @@
                      this.methodProfiler.exit();
                  }
  
@@ -145,7 +149,7 @@
                  if (iblockdata2 == iblockdata) {
                      if (iblockdata1 != iblockdata2) {
                          this.b(blockposition, iblockdata1, iblockdata2);
-@@ -151,12 +248,65 @@
+@@ -151,12 +252,65 @@
  
                      this.a(blockposition, iblockdata1, iblockdata2);
                  }
@@ -211,7 +215,7 @@
      public void a(BlockPosition blockposition, IBlockData iblockdata, IBlockData iblockdata1) {}
  
      @Override
-@@ -195,6 +345,11 @@
+@@ -195,6 +349,11 @@
      @Override
      public void update(BlockPosition blockposition, Block block) {
          if (this.worldData.getType() != WorldType.DEBUG_ALL_BLOCK_STATES) {
@@ -223,7 +227,7 @@
              this.applyPhysics(blockposition, block);
          }
  
-@@ -243,6 +398,17 @@
+@@ -243,6 +402,17 @@
              IBlockData iblockdata = this.getType(blockposition);
  
              try {
@@ -241,7 +245,7 @@
                  iblockdata.doPhysics(this, blockposition, block, blockposition1, false);
              } catch (Throwable throwable) {
                  CrashReport crashreport = CrashReport.a(throwable, "Exception while updating neighbours");
-@@ -285,6 +451,14 @@
+@@ -285,6 +455,14 @@
  
      @Override
      public IBlockData getType(BlockPosition blockposition) {
@@ -256,7 +260,7 @@
          if (isOutsideWorld(blockposition)) {
              return Blocks.VOID_AIR.getBlockData();
          } else {
-@@ -306,11 +480,11 @@
+@@ -306,11 +484,11 @@
      }
  
      public boolean isDay() {
@@ -270,7 +274,7 @@
      }
  
      @Override
-@@ -432,9 +606,11 @@
+@@ -432,9 +610,11 @@
                  TileEntity tileentity1 = (TileEntity) this.tileEntityListPending.get(i);
  
                  if (!tileentity1.isRemoved()) {
@@ -282,7 +286,7 @@
  
                      if (this.isLoaded(tileentity1.getPosition())) {
                          Chunk chunk = this.getChunkAtWorldCoords(tileentity1.getPosition());
-@@ -442,6 +618,12 @@
+@@ -442,6 +622,12 @@
  
                          chunk.setTileEntity(tileentity1.getPosition(), tileentity1);
                          this.notify(tileentity1.getPosition(), iblockdata, iblockdata, 3);
@@ -295,7 +299,7 @@
                      }
                  }
              }
-@@ -606,12 +788,25 @@
+@@ -606,12 +792,25 @@
  
      @Nullable
      @Override
@@ -321,7 +325,7 @@
              TileEntity tileentity = null;
  
              if (this.tickingTileEntities) {
-@@ -646,6 +841,13 @@
+@@ -646,6 +845,13 @@
      public void setTileEntity(BlockPosition blockposition, @Nullable TileEntity tileentity) {
          if (!isOutsideWorld(blockposition)) {
              if (tileentity != null && !tileentity.isRemoved()) {
@@ -335,7 +339,7 @@
                  if (this.tickingTileEntities) {
                      tileentity.setLocation(this, blockposition);
                      Iterator iterator = this.tileEntityListPending.iterator();
-@@ -670,7 +872,7 @@
+@@ -670,7 +876,7 @@
      }
  
      public void removeTileEntity(BlockPosition blockposition) {
diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/paper-server/src/main/java/org/bukkit/craftbukkit/CraftServer.java
index 8d1aba5354..e3d5b7bd25 100644
--- a/paper-server/src/main/java/org/bukkit/craftbukkit/CraftServer.java
+++ b/paper-server/src/main/java/org/bukkit/craftbukkit/CraftServer.java
@@ -639,6 +639,16 @@ public final class CraftServer implements Server {
         return this.configuration.getInt("ticks-per.monster-spawns");
     }
 
+    @Override
+    public int getTicksPerWaterSpawns() {
+        return this.configuration.getInt("ticks-per.water-spawns");
+    }
+
+    @Override
+    public int getTicksPerAmbientSpawns() {
+        return this.configuration.getInt("ticks-per.ambient-spawns");
+    }
+
     @Override
     public PluginManager getPluginManager() {
         return pluginManager;
@@ -751,6 +761,18 @@ public final class CraftServer implements Server {
             } else {
                 world.ticksPerMonsterSpawns = this.getTicksPerMonsterSpawns();
             }
+
+            if (this.getTicksPerWaterSpawns() < 0) {
+                world.ticksPerWaterSpawns = 1;
+            } else {
+                world.ticksPerWaterSpawns = this.getTicksPerWaterSpawns();
+            }
+
+            if (this.getTicksPerAmbientSpawns() < 0) {
+                world.ticksPerAmbientSpawns = 1;
+            } else {
+                world.ticksPerAmbientSpawns = this.getTicksPerAmbientSpawns();
+            }
         }
 
         pluginManager.clearPlugins();
diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/paper-server/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
index 32bcbd7b42..c1cdf857df 100644
--- a/paper-server/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
+++ b/paper-server/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
@@ -1920,6 +1920,26 @@ public class CraftWorld implements World {
         world.ticksPerMonsterSpawns = ticksPerMonsterSpawns;
     }
 
+    @Override
+    public long getTicksPerWaterSpawns() {
+        return world.ticksPerWaterSpawns;
+    }
+
+    @Override
+    public void setTicksPerWaterSpawns(int ticksPerWaterSpawns) {
+        world.ticksPerWaterSpawns = ticksPerWaterSpawns;
+    }
+
+    @Override
+    public long getTicksPerAmbientSpawns() {
+        return world.ticksPerAmbientSpawns;
+    }
+
+    @Override
+    public void setTicksPerAmbientSpawns(int ticksPerAmbientSpawns) {
+        world.ticksPerAmbientSpawns = ticksPerAmbientSpawns;
+    }
+
     @Override
     public void setMetadata(String metadataKey, MetadataValue newMetadataValue) {
         server.getWorldMetadata().setMetadata(this, metadataKey, newMetadataValue);
diff --git a/paper-server/src/main/resources/configurations/bukkit.yml b/paper-server/src/main/resources/configurations/bukkit.yml
index 39dbf58ceb..e9cee1713e 100644
--- a/paper-server/src/main/resources/configurations/bukkit.yml
+++ b/paper-server/src/main/resources/configurations/bukkit.yml
@@ -33,5 +33,7 @@ chunk-gc:
 ticks-per:
     animal-spawns: 400
     monster-spawns: 1
+    water-spawns: 1
+    ambient-spawns: 1
     autosave: 6000
 aliases: now-in-commands.yml