diff --git a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java
index e8490c240..c5fe7f2bf 100644
--- a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java
+++ b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java
@@ -37,7 +37,6 @@ import com.github.steveice10.mc.protocol.MinecraftProtocol;
 import com.github.steveice10.mc.protocol.codec.MinecraftCodecHelper;
 import com.github.steveice10.mc.protocol.data.ProtocolState;
 import com.github.steveice10.mc.protocol.data.UnexpectedEncryptionException;
-import com.github.steveice10.mc.protocol.data.game.MessageType;
 import com.github.steveice10.mc.protocol.data.game.entity.metadata.Pose;
 import com.github.steveice10.mc.protocol.data.game.entity.object.Direction;
 import com.github.steveice10.mc.protocol.data.game.entity.player.GameMode;
@@ -124,10 +123,12 @@ import org.geysermc.geyser.skin.FloodgateSkinUploader;
 import org.geysermc.geyser.text.ChatTypeEntry;
 import org.geysermc.geyser.text.GeyserLocale;
 import org.geysermc.geyser.text.MinecraftLocale;
-import org.geysermc.geyser.text.TextDecoration;
 import org.geysermc.geyser.translator.inventory.InventoryTranslator;
 import org.geysermc.geyser.translator.text.MessageTranslator;
-import org.geysermc.geyser.util.*;
+import org.geysermc.geyser.util.ChunkUtils;
+import org.geysermc.geyser.util.DimensionUtils;
+import org.geysermc.geyser.util.LoginEncryptionUtils;
+import org.geysermc.geyser.util.MathUtils;
 
 import javax.annotation.Nonnull;
 import java.net.ConnectException;
@@ -337,7 +338,7 @@ public class GeyserSession implements GeyserConnection, CommandSender {
      */
     private final Map<String, JavaDimension> dimensions = new Object2ObjectOpenHashMap<>(3);
 
-    private final Int2ObjectMap<ChatTypeEntry> chatTypes = new Int2ObjectOpenHashMap<>(7);
+    private final Int2ObjectMap<ChatTypeEntry> chatTypes = new Int2ObjectOpenHashMap<>(8);
 
     @Setter
     private int breakingBlock;
@@ -548,6 +549,8 @@ public class GeyserSession implements GeyserConnection, CommandSender {
         this.playerEntity = new SessionPlayerEntity(this);
         collisionManager.updatePlayerBoundingBox(this.playerEntity.getPosition());
 
+        ChatTypeEntry.applyDefaults(chatTypes);
+
         this.playerInventory = new PlayerInventory();
         this.openInventory = null;
         this.craftingRecipes = new Int2ObjectOpenHashMap<>();
diff --git a/core/src/main/java/org/geysermc/geyser/session/UpstreamSession.java b/core/src/main/java/org/geysermc/geyser/session/UpstreamSession.java
index 060dcc7fb..3250faf64 100644
--- a/core/src/main/java/org/geysermc/geyser/session/UpstreamSession.java
+++ b/core/src/main/java/org/geysermc/geyser/session/UpstreamSession.java
@@ -33,12 +33,15 @@ import lombok.RequiredArgsConstructor;
 import lombok.Setter;
 
 import java.net.InetSocketAddress;
+import java.util.ArrayDeque;
+import java.util.Queue;
 
 @RequiredArgsConstructor
 public class UpstreamSession {
     @Getter private final BedrockServerSession session;
     @Getter @Setter
     private boolean initialized = false;
+    private Queue<BedrockPacket> postStartGamePackets = new ArrayDeque<>();
 
     public void sendPacket(@NonNull BedrockPacket packet) {
         if (!isClosed()) {
@@ -56,6 +59,25 @@ public class UpstreamSession {
         session.disconnect(reason);
     }
 
+    /**
+     * Queue a packet that must be delayed until after login.
+     */
+    public void queuePostStartGamePacket(BedrockPacket packet) {
+        postStartGamePackets.add(packet);
+    }
+
+    public void sendPostStartGamePackets() {
+        if (isClosed()) {
+            return;
+        }
+
+        BedrockPacket packet;
+        while ((packet = postStartGamePackets.poll()) != null) {
+            session.sendPacket(packet);
+        }
+        postStartGamePackets = null;
+    }
+
     public boolean isClosed() {
         return session.isClosed();
     }
diff --git a/core/src/main/java/org/geysermc/geyser/text/ChatTypeEntry.java b/core/src/main/java/org/geysermc/geyser/text/ChatTypeEntry.java
index 32be209ca..800eb6c0f 100644
--- a/core/src/main/java/org/geysermc/geyser/text/ChatTypeEntry.java
+++ b/core/src/main/java/org/geysermc/geyser/text/ChatTypeEntry.java
@@ -25,10 +25,33 @@
 
 package org.geysermc.geyser.text;
 
+import com.github.steveice10.mc.protocol.data.game.MessageType;
 import com.nukkitx.protocol.bedrock.packet.TextPacket;
+import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
 public record ChatTypeEntry(@Nonnull TextPacket.Type bedrockChatType, @Nullable TextDecoration textDecoration) {
+    private static final ChatTypeEntry CHAT = new ChatTypeEntry(TextPacket.Type.CHAT, null);
+    private static final ChatTypeEntry SYSTEM = new ChatTypeEntry(TextPacket.Type.CHAT, null);
+    private static final ChatTypeEntry TIP = new ChatTypeEntry(TextPacket.Type.CHAT, null);
+    private static final ChatTypeEntry RAW = new ChatTypeEntry(TextPacket.Type.CHAT, null);
+
+    /**
+     * Apply defaults to a map so it isn't empty in the event a chat message is sent before the login packet.
+     */
+    public static void applyDefaults(Int2ObjectMap<ChatTypeEntry> chatTypes) {
+        // So the proper way to do this, probably, would be to dump the NBT data from vanilla and load it.
+        // But, the only way this happens is if a chat message is sent to us before the login packet, which is rare.
+        // So we'll just make sure chat ends up in the right place.
+        chatTypes.put(MessageType.CHAT.ordinal(), CHAT);
+        chatTypes.put(MessageType.SYSTEM.ordinal(), SYSTEM);
+        chatTypes.put(MessageType.GAME_INFO.ordinal(), TIP);
+        chatTypes.put(MessageType.SAY_COMMAND.ordinal(), RAW);
+        chatTypes.put(MessageType.MSG_COMMAND.ordinal(), RAW);
+        chatTypes.put(MessageType.TEAM_MSG_COMMAND.ordinal(), RAW);
+        chatTypes.put(MessageType.EMOTE_COMMAND.ordinal(), RAW);
+        chatTypes.put(MessageType.TELLRAW_COMMAND.ordinal(), RAW);
+    }
 }
diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaLoginTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaLoginTranslator.java
index 74d12bd19..cd26e53e5 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaLoginTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaLoginTranslator.java
@@ -115,6 +115,9 @@ public class JavaLoginTranslator extends PacketTranslator<ClientboundLoginPacket
             // The player has yet to spawn so let's do that using some of the information in this Java packet
             session.setDimension(newDimension);
             session.connect();
+
+            // It is now safe to send these packets
+            session.getUpstream().sendPostStartGamePackets();
         }
 
         AdventureSettingsPacket bedrockPacket = new AdventureSettingsPacket();
diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaSystemChatTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaSystemChatTranslator.java
index fc4a32bac..2d69c363d 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaSystemChatTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaSystemChatTranslator.java
@@ -46,6 +46,10 @@ public class JavaSystemChatTranslator extends PacketTranslator<ClientboundSystem
         textPacket.setNeedsTranslation(false);
         textPacket.setMessage(MessageTranslator.convertMessage(packet.getContent(), session.getLocale()));
 
-        session.sendUpstreamPacket(textPacket);
+        if (session.isSentSpawnPacket()) {
+            session.sendUpstreamPacket(textPacket);
+        } else {
+            session.getUpstream().queuePostStartGamePacket(textPacket);
+        }
     }
 }