diff --git a/core/src/main/java/org/geysermc/geyser/session/cache/WorldCache.java b/core/src/main/java/org/geysermc/geyser/session/cache/WorldCache.java
index c84126608..fadd78cbe 100644
--- a/core/src/main/java/org/geysermc/geyser/session/cache/WorldCache.java
+++ b/core/src/main/java/org/geysermc/geyser/session/cache/WorldCache.java
@@ -168,6 +168,12 @@ public final class WorldCache {
         ChunkUtils.updateBlock(session, blockState, position);
     }
 
+    public void removePrediction(Vector3i position) {
+        if (!this.unverifiedPredictions.isEmpty()) {
+            this.unverifiedPredictions.removeInt(position);
+        }
+    }
+
     public void endPredictionsUpTo(int sequence) {
         if (this.unverifiedPredictions.isEmpty()) {
             return;
diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaSectionBlocksUpdateTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaSectionBlocksUpdateTranslator.java
index a52bb33b0..6ebc446f8 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaSectionBlocksUpdateTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaSectionBlocksUpdateTranslator.java
@@ -25,19 +25,190 @@
 
 package org.geysermc.geyser.translator.protocol.java.level;
 
+import org.cloudburstmc.math.vector.Vector3i;
+import org.cloudburstmc.protocol.bedrock.data.definitions.BlockDefinition;
+import org.cloudburstmc.protocol.bedrock.packet.UpdateBlockPacket;
+import org.cloudburstmc.protocol.bedrock.packet.UpdateSubChunkBlocksPacket;
+import org.geysermc.erosion.util.BlockPositionIterator;
+import org.geysermc.geyser.entity.type.ItemFrameEntity;
+import org.geysermc.geyser.level.block.BlockStateValues;
+import org.geysermc.geyser.registry.BlockRegistries;
+import org.geysermc.geyser.session.cache.SkullCache;
+import org.geysermc.geyser.translator.level.block.entity.BedrockOnlyBlockEntity;
+import org.geysermc.geyser.util.BlockEntityUtils;
 import org.geysermc.mcprotocollib.protocol.data.game.level.block.BlockChangeEntry;
 import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.level.ClientboundSectionBlocksUpdatePacket;
 import org.geysermc.geyser.session.GeyserSession;
 import org.geysermc.geyser.translator.protocol.PacketTranslator;
 import org.geysermc.geyser.translator.protocol.Translator;
 
+import java.util.BitSet;
+
+import static org.geysermc.geyser.level.block.BlockStateValues.JAVA_AIR_ID;
+
 @Translator(packet = ClientboundSectionBlocksUpdatePacket.class)
 public class JavaSectionBlocksUpdateTranslator extends PacketTranslator<ClientboundSectionBlocksUpdatePacket> {
+    private static final int NEIGHBORS_NETWORK_FLAG = (1 << UpdateBlockPacket.Flag.NEIGHBORS.ordinal()) | (1 << UpdateBlockPacket.Flag.NETWORK.ordinal());
+    private static final int NETWORK_FLAG = (1 << UpdateBlockPacket.Flag.NETWORK.ordinal());
 
     @Override
     public void translate(GeyserSession session, ClientboundSectionBlocksUpdatePacket packet) {
+        // Send normal block updates if not many changes
+        if (packet.getEntries().length < 32) {
+            for (BlockChangeEntry entry : packet.getEntries()) {
+                session.getWorldCache().updateServerCorrectBlockState(entry.getPosition(), entry.getBlock());
+            }
+            return;
+        }
+
+        UpdateSubChunkBlocksPacket subChunkBlocksPacket = new UpdateSubChunkBlocksPacket();
+        subChunkBlocksPacket.setChunkX(packet.getChunkX());
+        subChunkBlocksPacket.setChunkY(packet.getChunkY());
+        subChunkBlocksPacket.setChunkZ(packet.getChunkZ());
+
+        // If the entire section is updated, this might be a legacy non-full chunk update
+        // which can contain thousands of unchanged blocks
+        if (packet.getEntries().length == 4096) {
+            // hack - bedrock might ignore the block updates if the chunk was still loading.
+            // sending an UpdateBlockPacket seems to force it
+            BlockChangeEntry firstEntry = packet.getEntries()[0];
+            UpdateBlockPacket blockPacket = new UpdateBlockPacket();
+            blockPacket.setBlockPosition(firstEntry.getPosition());
+            blockPacket.setDefinition(session.getBlockMappings().getBedrockBlock(firstEntry.getBlock()));
+            blockPacket.setDataLayer(0);
+            session.sendUpstreamPacket(blockPacket);
+
+            // Filter out unchanged blocks
+            Vector3i offset = Vector3i.from(packet.getChunkX() << 4, packet.getChunkY() << 4, packet.getChunkZ() << 4);
+            BlockPositionIterator blockIter = BlockPositionIterator.fromMinMax(
+                    offset.getX(), offset.getY(), offset.getZ(),
+                    offset.getX() + 15, offset.getY() + 15, offset.getZ() + 15
+            );
+
+            int[] sectionBlocks = session.getGeyser().getWorldManager().getBlocksAt(session, blockIter);
+            BitSet waterlogged = BlockRegistries.WATERLOGGED.get();
+            for (BlockChangeEntry entry : packet.getEntries()) {
+                Vector3i pos = entry.getPosition().sub(offset);
+                int index = pos.getZ() + pos.getX() * 16 + pos.getY() * 256;
+                int oldBlockState = sectionBlocks[index];
+                if (oldBlockState != entry.getBlock()) {
+                    // Avoid sending unnecessary waterlogged updates
+                    boolean updateWaterlogged = waterlogged.get(oldBlockState) != waterlogged.get(entry.getBlock());
+                    applyEntry(session, entry, subChunkBlocksPacket, updateWaterlogged);
+                }
+            }
+        } else {
+            for (BlockChangeEntry entry : packet.getEntries()) {
+                applyEntry(session, entry, subChunkBlocksPacket, true);
+            }
+        }
+
+        session.sendUpstreamPacket(subChunkBlocksPacket);
+
+        // Post block update
         for (BlockChangeEntry entry : packet.getEntries()) {
-            session.getWorldCache().updateServerCorrectBlockState(entry.getPosition(), entry.getBlock());
+            session.getWorldCache().removePrediction(entry.getPosition());
+            BlockStateValues.getLecternBookStates().handleBlockChange(session, entry.getBlock(), entry.getPosition());
+
+            // Iterates through all Bedrock-only block entity translators and determines if a manual block entity packet
+            // needs to be sent
+            for (BedrockOnlyBlockEntity bedrockOnlyBlockEntity : BlockEntityUtils.BEDROCK_ONLY_BLOCK_ENTITIES) {
+                if (bedrockOnlyBlockEntity.isBlock(entry.getBlock())) {
+                    // Flower pots are block entities only in Bedrock and are not updated anywhere else like note blocks
+                    bedrockOnlyBlockEntity.updateBlock(session, entry.getBlock(), entry.getPosition());
+                    break; //No block will be a part of two classes
+                }
+            }
+        }
+    }
+
+    // Modified version of ChunkUtils#updateBlockClientSide
+    private static void applyEntry(GeyserSession session, BlockChangeEntry entry, UpdateSubChunkBlocksPacket subChunkBlocksPacket, boolean updateWaterlogged) {
+        Vector3i position = entry.getPosition();
+        int blockState = entry.getBlock();
+
+        session.getChunkCache().updateBlock(position.getX(), position.getY(), position.getZ(), blockState);
+
+        // Checks for item frames so they aren't tripped up and removed
+        ItemFrameEntity itemFrameEntity = ItemFrameEntity.getItemFrameEntity(session, position);
+        if (itemFrameEntity != null) {
+            if (blockState == JAVA_AIR_ID) { // Item frame is still present and no block overrides that; refresh it
+                itemFrameEntity.updateBlock(true);
+                // Still update the chunk cache with the new block if updateBlock is called
+                return;
+            }
+            // Otherwise, let's still store our reference to the item frame, but let the new block take precedence for now
+        }
+
+        BlockDefinition definition = session.getBlockMappings().getBedrockBlock(blockState);
+
+        int skullVariant = BlockStateValues.getSkullVariant(blockState);
+        if (skullVariant == -1) {
+            // Skull is gone
+            session.getSkullCache().removeSkull(position);
+        } else if (skullVariant == 3) {
+            // The changed block was a player skull so check if a custom block was defined for this skull
+            SkullCache.Skull skull = session.getSkullCache().updateSkull(position, blockState);
+            if (skull != null && skull.getBlockDefinition() != null) {
+                definition = skull.getBlockDefinition();
+            }
+        }
+
+        // Prevent moving_piston from being placed
+        // It's used for extending piston heads, but it isn't needed on Bedrock and causes pistons to flicker
+        if (!BlockStateValues.isMovingPiston(blockState)) {
+            subChunkBlocksPacket.getStandardBlocks().add(new org.cloudburstmc.protocol.bedrock.data.BlockChangeEntry(
+                    position,
+                    definition,
+                    NEIGHBORS_NETWORK_FLAG,
+                    -1,
+                    org.cloudburstmc.protocol.bedrock.data.BlockChangeEntry.MessageType.NONE
+            ));
+
+            if (updateWaterlogged) {
+                BlockDefinition waterDefinition = BlockRegistries.WATERLOGGED.get().get(blockState) ?
+                        session.getBlockMappings().getBedrockWater() : session.getBlockMappings().getBedrockAir();
+                subChunkBlocksPacket.getExtraBlocks().add(new org.cloudburstmc.protocol.bedrock.data.BlockChangeEntry(
+                        position,
+                        waterDefinition,
+                        0,
+                        -1,
+                        org.cloudburstmc.protocol.bedrock.data.BlockChangeEntry.MessageType.NONE
+                ));
+            }
+        }
+
+        // Extended collision boxes for custom blocks
+        if (!session.getBlockMappings().getExtendedCollisionBoxes().isEmpty()) {
+            int aboveBlock = session.getGeyser().getWorldManager().getBlockAt(session, position.getX(), position.getY() + 1, position.getZ());
+            BlockDefinition aboveBedrockExtendedCollisionDefinition = session.getBlockMappings().getExtendedCollisionBoxes().get(blockState);
+            int belowBlock = session.getGeyser().getWorldManager().getBlockAt(session, position.getX(), position.getY() - 1, position.getZ());
+            BlockDefinition belowBedrockExtendedCollisionDefinition = session.getBlockMappings().getExtendedCollisionBoxes().get(belowBlock);
+            if (belowBedrockExtendedCollisionDefinition != null && blockState == BlockStateValues.JAVA_AIR_ID) {
+                subChunkBlocksPacket.getStandardBlocks().add(new org.cloudburstmc.protocol.bedrock.data.BlockChangeEntry(
+                        position,
+                        belowBedrockExtendedCollisionDefinition,
+                        NETWORK_FLAG,
+                        -1,
+                        org.cloudburstmc.protocol.bedrock.data.BlockChangeEntry.MessageType.NONE
+                ));
+            } else if (aboveBedrockExtendedCollisionDefinition != null && aboveBlock == BlockStateValues.JAVA_AIR_ID) {
+                subChunkBlocksPacket.getStandardBlocks().add(new org.cloudburstmc.protocol.bedrock.data.BlockChangeEntry(
+                        position.add(0, 1, 0),
+                        aboveBedrockExtendedCollisionDefinition,
+                        NETWORK_FLAG,
+                        -1,
+                        org.cloudburstmc.protocol.bedrock.data.BlockChangeEntry.MessageType.NONE
+                ));
+            } else if (aboveBlock == BlockStateValues.JAVA_AIR_ID) {
+                subChunkBlocksPacket.getStandardBlocks().add(new org.cloudburstmc.protocol.bedrock.data.BlockChangeEntry(
+                        position.add(0, 1, 0),
+                        session.getBlockMappings().getBedrockAir(),
+                        NETWORK_FLAG,
+                        -1,
+                        org.cloudburstmc.protocol.bedrock.data.BlockChangeEntry.MessageType.NONE
+                ));
+            }
         }
     }
 }