diff --git a/src/c_console.cpp b/src/c_console.cpp
index caea2a741..8d2d1dfca 100644
--- a/src/c_console.cpp
+++ b/src/c_console.cpp
@@ -629,7 +629,7 @@ void C_DeinitConsole ()
 	while (cmd != NULL)
 	{
 		GameAtExit *next = cmd->Next;
-		AddCommandString (cmd->Command.LockBuffer());
+		AddCommandString (cmd->Command);
 		delete cmd;
 		cmd = next;
 	}
@@ -1559,17 +1559,9 @@ static bool C_HandleKey (event_t *ev, FCommandBuffer &buffer)
 			{
 				// Work with a copy of command to avoid side effects caused by
 				// exception raised during execution, like with 'error' CCMD.
-				// It's problematic to maintain FString's lock symmetry.
-				static TArray<char> command;
-				const size_t length = buffer.Text.Len();
-
-				command.Resize(unsigned(length + 1));
-				memcpy(&command[0], buffer.Text.GetChars(), length);
-				command[length] = '\0';
-
+				FString copy = buffer.Text;
 				buffer.SetString("");
-
-				AddCommandString(&command[0]);
+				AddCommandString(copy);
 			}
 			TabbedLast = false;
 			TabbedList = false;
diff --git a/src/c_dispatch.cpp b/src/c_dispatch.cpp
index b046f4467..b91ad83cc 100644
--- a/src/c_dispatch.cpp
+++ b/src/c_dispatch.cpp
@@ -59,38 +59,119 @@
 
 // TYPES -------------------------------------------------------------------
 
-class DWaitingCommand : public DThinker
+class UnsafeExecutionScope
 {
-	DECLARE_CLASS (DWaitingCommand, DThinker)
+	const bool wasEnabled;
+
 public:
-	DWaitingCommand (const char *cmd, int tics, bool unsafe);
-	~DWaitingCommand ();
-	void Serialize(FSerializer &arc);
-	void Tick ();
+	explicit UnsafeExecutionScope(const bool enable = true)
+		: wasEnabled(UnsafeExecutionContext)
+	{
+		UnsafeExecutionContext = enable;
+	}
 
-private:
-	DWaitingCommand ();
+	~UnsafeExecutionScope()
+	{
+		UnsafeExecutionContext = wasEnabled;
+	}
+};
 
-	char *Command;
+class FDelayedCommand
+{
+protected:
+	virtual bool Tick() = 0;
+
+	friend class FDelayedCommandQueue;
+};
+
+class FWaitingCommand : public FDelayedCommand
+{
+public:
+	FWaitingCommand(const char *cmd, int tics, bool unsafe)
+		: Command(cmd), TicsLeft(tics+1), IsUnsafe(unsafe)
+	{}
+
+	bool Tick() override
+	{
+		if (--TicsLeft == 0)
+		{
+			UnsafeExecutionScope scope(IsUnsafe);
+			AddCommandString(Command);
+			return true;
+		}
+		return false;
+	}
+
+	FString Command;
 	int TicsLeft;
 	bool IsUnsafe;
 };
 
-class DStoredCommand : public DThinker
+class FStoredCommand : public FDelayedCommand
 {
-	DECLARE_CLASS (DStoredCommand, DThinker)
 public:
-	DStoredCommand (FConsoleCommand *com, const char *cmd);
-	~DStoredCommand ();
-	void Tick ();
+	FStoredCommand(FConsoleCommand *com, const char *cmd)
+		: Command(com), Text(cmd)
+	{}
+
+	bool Tick() override
+	{
+		if (Text.IsNotEmpty() && Command != nullptr)
+		{
+			FCommandLine args(Text);
+			Command->Run(args, players[consoleplayer].mo, 0);
+		}
+		return true;
+	}
 
 private:
-	DStoredCommand ();
 
 	FConsoleCommand *Command;
-	char *Text;
+	FString Text;
 };
 
+class FDelayedCommandQueue
+{
+	TDeletingArray<FDelayedCommand *> delayedCommands;
+public:
+	void Run()
+	{
+		for (unsigned i = 0; i < delayedCommands.Size(); i++)
+		{
+			if (delayedCommands[i]->Tick())
+			{
+				delete delayedCommands[i];
+				delayedCommands.Delete(i);
+				i--;
+			}
+		}
+	}
+
+	void Clear()
+	{
+		delayedCommands.DeleteAndClear();
+	}
+
+	void AddCommand(FDelayedCommand * cmd)
+	{
+		delayedCommands.Push(cmd);
+	}
+};
+
+static FDelayedCommandQueue delayedCommandQueue;
+
+void C_RunDelayedCommands()
+{
+	delayedCommandQueue.Run();
+}
+
+void C_ClearDelayedCommands()
+{
+	delayedCommandQueue.Clear();
+}
+
+
+
 struct FActionMap
 {
 	FButtonStatus	*Button;
@@ -127,22 +208,6 @@ FButtonStatus Button_Mlook, Button_Klook, Button_Use, Button_AltAttack,
 
 bool ParsingKeyConf, UnsafeExecutionContext;
 
-class UnsafeExecutionScope
-{
-	const bool wasEnabled;
-
-public:
-	explicit UnsafeExecutionScope(const bool enable = true)
-	: wasEnabled(UnsafeExecutionContext)
-	{
-		UnsafeExecutionContext = enable;
-	}
-
-	~UnsafeExecutionScope()
-	{
-		UnsafeExecutionContext = wasEnabled;
-	}
-};
 
 // To add new actions, go to the console and type "key <action name>".
 // This will give you the key value to use in the first column. Then
@@ -205,79 +270,6 @@ static const char *KeyConfCommands[] =
 
 // CODE --------------------------------------------------------------------
 
-IMPLEMENT_CLASS(DWaitingCommand, false, false)
-
-void DWaitingCommand::Serialize(FSerializer &arc)
-{
-	Super::Serialize (arc);
-	arc("command", Command)
-		("ticsleft", TicsLeft)
-		("unsafe", IsUnsafe);
-}
-
-DWaitingCommand::DWaitingCommand ()
-{
-	Command = NULL;
-	TicsLeft = 1;
-	IsUnsafe = false;
-}
-
-DWaitingCommand::DWaitingCommand (const char *cmd, int tics, bool unsafe)
-{
-	Command = copystring (cmd);
-	TicsLeft = tics+1;
-	IsUnsafe = unsafe;
-}
-
-DWaitingCommand::~DWaitingCommand ()
-{
-	if (Command != NULL)
-	{
-		delete[] Command;
-	}
-}
-
-void DWaitingCommand::Tick ()
-{
-	if (--TicsLeft == 0)
-	{
-		UnsafeExecutionScope scope(IsUnsafe);
-		AddCommandString (Command);
-		Destroy ();
-	}
-}
-
-IMPLEMENT_CLASS(DStoredCommand, false, false)
-
-DStoredCommand::DStoredCommand ()
-{
-	Text = NULL;
-	Destroy ();
-}
-
-DStoredCommand::DStoredCommand (FConsoleCommand *command, const char *args)
-{
-	Command = command;
-	Text = copystring (args);
-}		
-
-DStoredCommand::~DStoredCommand ()
-{
-	if (Text != NULL)
-	{
-		delete[] Text;
-	}
-}
-
-void DStoredCommand::Tick ()
-{
-	if (Text != NULL && Command != NULL)
-	{
-		FCommandLine args (Text);
-		Command->Run (args, players[consoleplayer].mo, 0);
-	}
-	Destroy ();
-}
 
 static int ListActionCommands (const char *pattern)
 {
@@ -656,7 +648,8 @@ void C_DoCommand (const char *cmd, int keynum)
 			}
 			else
 			{
-				Create<DStoredCommand> (com, beg);
+				auto cmd = new FStoredCommand(com, beg);
+				delayedCommandQueue.AddCommand(cmd);
 			}
 		}
 	}
@@ -696,8 +689,12 @@ DEFINE_ACTION_FUNCTION(DOptionMenuItemCommand, DoCommand)
 	return 0;
 }
 
-void AddCommandString (char *cmd, int keynum)
+void AddCommandString (const char *text, int keynum)
 {
+	// Operate on a local copy instead of messing around with the data that's being passed in here.
+	TArray<char> buffer(strlen(text) + 1, true);
+	memcpy(buffer.Data(), text, buffer.Size());
+	char *cmd = buffer.Data();
 	char *brkpt;
 	int more;
 
@@ -752,7 +749,8 @@ void AddCommandString (char *cmd, int keynum)
 						  // Note that deferred commands lose track of which key
 						  // (if any) they were pressed from.
 							*brkpt = ';';
-							Create<DWaitingCommand> (brkpt, tics, UnsafeExecutionContext);
+							auto cmd = new FWaitingCommand(brkpt, tics, UnsafeExecutionContext);
+							delayedCommandQueue.AddCommand(cmd);
 						}
 						return;
 					}
@@ -1477,8 +1475,7 @@ void FConsoleAlias::Run (FCommandLine &args, AActor *who, int key)
 	}
 
 	bRunning = true;
-	AddCommandString (mycommand.LockBuffer(), key);
-	mycommand.UnlockBuffer();
+	AddCommandString (mycommand, key);
 	bRunning = false;
 	if (m_Command[index].IsEmpty())
 	{ // The alias is unchanged, so put the command back so it can be used again.
@@ -1554,8 +1551,7 @@ void FExecList::ExecCommands() const
 {
 	for (unsigned i = 0; i < Commands.Size(); ++i)
 	{
-		AddCommandString(Commands[i].LockBuffer());
-		Commands[i].UnlockBuffer();
+		AddCommandString(Commands[i]);
 	}
 }
 
diff --git a/src/c_dispatch.h b/src/c_dispatch.h
index 101bd8c63..e21e977d1 100644
--- a/src/c_dispatch.h
+++ b/src/c_dispatch.h
@@ -77,7 +77,10 @@ FExecList *C_ParseCmdLineParams(FExecList *exec);
 // and semicolon-separated commands. This function may modify the source
 // string, but the string will be restored to its original state before
 // returning. Therefore, commands passed must not be in read-only memory.
-void AddCommandString (char *text, int keynum=0);
+void AddCommandString (const char *text, int keynum=0);
+
+void C_RunDelayedCommands();
+void C_ClearDelayedCommands();
 
 // Process a single console command. Does not handle wait.
 void C_DoCommand (const char *cmd, int keynum=0);
diff --git a/src/d_main.cpp b/src/d_main.cpp
index 82189c72a..b5aaced2d 100644
--- a/src/d_main.cpp
+++ b/src/d_main.cpp
@@ -2579,7 +2579,7 @@ void D_DoomMain (void)
 		// [RH] Run any saved commands from the command line or autoexec.cfg now.
 		gamestate = GS_FULLCONSOLE;
 		Net_NewMakeTic ();
-		DThinker::RunThinkers ();
+		C_RunDelayedCommands();
 		gamestate = GS_STARTUP;
 
 		if (!restart)
@@ -2646,7 +2646,7 @@ void D_DoomMain (void)
 							G_InitNew(startmap, false);
 							if (StoredWarp.IsNotEmpty())
 							{
-								AddCommandString(StoredWarp.LockBuffer());
+								AddCommandString(StoredWarp);
 								StoredWarp = NULL;
 							}
 						}
diff --git a/src/g_game.cpp b/src/g_game.cpp
index 49551e4ad..56029333e 100644
--- a/src/g_game.cpp
+++ b/src/g_game.cpp
@@ -1021,9 +1021,8 @@ void G_Ticker ()
 
 	if (ToggleFullscreen)
 	{
-		static char toggle_fullscreen[] = "toggle fullscreen";
 		ToggleFullscreen = false;
-		AddCommandString (toggle_fullscreen);
+		AddCommandString ("toggle fullscreen");
 	}
 
 	// do things to change the game state
@@ -1173,6 +1172,7 @@ void G_Ticker ()
 
 	// [ZZ] also tick the UI part of the events
 	E_UiTick();
+	C_RunDelayedCommands();
 
 	// do main actions
 	switch (gamestate)
diff --git a/src/g_inventory/a_weapons.cpp b/src/g_inventory/a_weapons.cpp
index 8bb2aace3..fe5761222 100644
--- a/src/g_inventory/a_weapons.cpp
+++ b/src/g_inventory/a_weapons.cpp
@@ -748,8 +748,7 @@ void P_PlaybackKeyConfWeapons(FWeaponSlots *slots)
 	PlayingKeyConf = slots;
 	for (unsigned int i = 0; i < KeyConfWeapons.Size(); ++i)
 	{
-		FString cmd(KeyConfWeapons[i]);
-		AddCommandString(cmd.LockBuffer());
+		AddCommandString(KeyConfWeapons[i]);
 	}
 	PlayingKeyConf = nullptr;
 }
diff --git a/src/sound/oalsound.cpp b/src/sound/oalsound.cpp
index ed3098d9f..697a7a91f 100644
--- a/src/sound/oalsound.cpp
+++ b/src/sound/oalsound.cpp
@@ -2202,8 +2202,7 @@ void OpenALSoundRenderer::UpdateSounds()
 		if(connected == ALC_FALSE)
 		{
 			Printf("Sound device disconnected; restarting...\n");
-			static char snd_reset[] = "snd_reset";
-			AddCommandString(snd_reset);
+			AddCommandString("snd_reset");
 			return;
 		}
 	}