--- a/src/framework/async/MsgChannel.h
+++ b/src/framework/async/MsgChannel.h
@@ -110,6 +110,12 @@
 
 					// Removes any pending outgoing or incoming reliable messages.
 	void			ClearReliableMessages( void );
+
+					// Returns the bytes currently awaiting acknowledgement.
+	int				GetPendingReliableBytes( void ) const { return reliableSend.GetTotalSize(); }
+	bool			CanSendMessage( int unreliableBytes ) const {
+		return 4 + reliableSend.GetTotalSize() + 4 + unreliableBytes <= MAX_MESSAGE_SIZE;
+	}
 private:
 	netadr_t		remoteAddress;	// address of remote host
 	int				id;				// our identification used instead of port number
--- a/src/framework/async/AsyncServer.h
+++ b/src/framework/async/AsyncServer.h
@@ -125,6 +125,10 @@
 
 	char				guid[12];  // Even Balance - M. Quinn
 	idStr				botCharacter;	// canonical bot profile, independent of the display name
+	idList<byte>		replayReliableBacklog;
+	int					replayReliableOffset;
+	int					replayReliableBlockedTime;
+	bool				replayReliableBarrier;
 
 } serverClient_t;
 
@@ -238,6 +242,7 @@
 	idStr				serverDemoPlaybackPath;
 	int					serverDemoPlaybackInitialGameTime;
 	int					serverDemoPlaybackStartTime;
+	int					serverDemoPlaybackLastTime;
 	userOrigin_t		serverDemoViewerOrigins[MAX_ASYNC_CLIENTS];
 	idDict				serverDemoViewerInfo[MAX_ASYNC_CLIENTS];
 
@@ -283,6 +288,12 @@
 	void				SendGameInitToClient( int clientNum );
 	bool				SendSnapshotToClient( int clientNum );
 	bool				SendRepeaterSnapshotToClient( int clientNum );
+	void				ClearReplayReliableBacklog( int clientNum );
+	bool				QueueReplayReliableMessage( int clientNum, const idBitMsg &msg );
+	bool				FlushReplayReliableBacklog( int clientNum );
+	bool				ReplayClientHasPendingReliable( int clientNum ) const;
+	void				RefreshReplayReliableBarriers( void );
+	bool				HasReadyReplayViewer( void ) const;
 	void				WriteServerDemoSnapshot( void );
 	void				RunServerDemoPlaybackFrame( void );
 	void				RecordServerDemoReliable( int recordType, int clientNum, const idBitMsg &msg );
--- a/src/framework/async/AsyncServer.cpp
+++ b/src/framework/async/AsyncServer.cpp
@@ -50,6 +50,10 @@
 static const int SERVER_DEMO_RECORD_SNAPSHOT = 2;
 static const int SERVER_DEMO_RECORD_END = 3;
 static const int SERVER_DEMO_MAX_SNAPSHOT = 4 * 1024 * 1024;
+static const int SERVER_DEMO_RELIABLE_SNAPSHOT_LIMIT = MAX_MESSAGE_SIZE / 4;
+static const int SERVER_DEMO_RELIABLE_BATCH_LIMIT = MAX_MESSAGE_SIZE - 256;
+static const int SERVER_DEMO_RELIABLE_BACKLOG_LIMIT = 1024 * 1024;
+static const int SERVER_DEMO_RELIABLE_STALL_MSEC = 30 * 1000;
 
 static idStr ServerDemoUtcStamp( const char *format ) {
 	char stamp[ 64 ];
@@ -150,6 +154,7 @@
 	serverDemoPlaybackFile = NULL;
 	serverDemoPlaybackInitialGameTime = 0;
 	serverDemoPlaybackStartTime = 0;
+	serverDemoPlaybackLastTime = 0;
 	memset( serverDemoViewerOrigins, 0, sizeof( serverDemoViewerOrigins ) );
 	memset( challenges, 0, sizeof( challenges ) );
 	memset( userCmds, 0, sizeof( userCmds ) );
@@ -722,6 +727,7 @@
 	game->SetServerInfo( serverInfo );
 	game->ReadNetworkInfo( initialGameTime, serverDemoPlaybackFile, MAX_ASYNC_CLIENTS );
 	serverDemoPlaybackStartTime = serverTime;
+	serverDemoPlaybackLastTime = serverTime;
 	serverDemoPlaybackLoading = false;
 	MasterHeartbeat( true );
 	common->Printf( "serving full-state server demo %s on port %d (recorded %s)\n",
@@ -747,6 +753,7 @@
 	serverDemoPlaybackEnded = false;
 	serverDemoPlaybackLoading = false;
 	serverDemoPlaybackStarted = false;
+	serverDemoPlaybackLastTime = 0;
 	serverDemoPlaybackPath.Clear();
 }
 
@@ -759,19 +766,29 @@
 	if ( !serverDemoPlaybackActive || serverDemoPlaybackLoading || serverDemoPlaybackEnded || serverDemoPlaybackFile == NULL ) {
 		return;
 	}
-	if ( !serverDemoPlaybackStarted ) {
-		for ( int i = 0; i < MAX_ASYNC_CLIENTS; ++i ) {
-			if ( clients[i].clientState == SCS_INGAME ) {
-				serverDemoPlaybackStarted = true;
-				serverDemoPlaybackStartTime = serverTime;
-				common->Printf( "starting network server demo %s for first viewer\n", serverDemoPlaybackPath.c_str() );
-				break;
-			}
+	RefreshReplayReliableBarriers();
+	const int playbackMsec = Max( 0, serverTime - serverDemoPlaybackLastTime );
+	serverDemoPlaybackLastTime = serverTime;
+	const bool hasReadyViewer = HasReadyReplayViewer();
+	bool hasInGameViewer = false;
+	for ( int i = 0; i < MAX_ASYNC_CLIENTS; ++i ) {
+		if ( clients[ i ].clientState == SCS_INGAME ) {
+			hasInGameViewer = true;
+			break;
 		}
-		if ( !serverDemoPlaybackStarted ) {
+	}
+	if ( !serverDemoPlaybackStarted ) {
+		if ( !hasReadyViewer ) {
 			game->RepeaterFrame( serverDemoViewerOrigins, true, gameFrame );
 			return;
 		}
+		serverDemoPlaybackStarted = true;
+		serverDemoPlaybackStartTime = serverTime;
+		common->Printf( "starting network server demo %s for first ready viewer\n", serverDemoPlaybackPath.c_str() );
+	} else if ( idAsyncNetwork::repeaterWaitForViewer.GetBool() && hasInGameViewer && !hasReadyViewer ) {
+		serverDemoPlaybackStartTime += playbackMsec;
+		game->RepeaterFrame( serverDemoViewerOrigins, true, gameFrame );
+		return;
 	}
 
 	const int targetGameTime = serverDemoPlaybackInitialGameTime + Max( 0, serverTime - serverDemoPlaybackStartTime );
@@ -1197,6 +1214,7 @@
 */
 void idAsyncServer::ClearClient( int clientNum ) {
 	serverClient_t &client = clients[clientNum];
+	ClearReplayReliableBacklog( clientNum );
 	client.clientId = 0;
 	client.clientState = SCS_FREE;
 	client.clientPrediction = 0;
@@ -1226,6 +1244,7 @@
 */
 void idAsyncServer::InitClient( int clientNum, int clientId, int clientRate ) {
 	int i;
+	ClearReplayReliableBacklog( clientNum );
 
 	// clear the user info
 	if ( IsServerDemoPlaying() ) {
@@ -1345,6 +1364,9 @@
 	if ( client.clientState <= SCS_ZOMBIE ) {
 		return;
 	}
+	if ( IsServerDemoPlaying() ) {
+		ClearReplayReliableBacklog( clientNum );
+	}
 
 	if ( client.clientState >= SCS_PUREWAIT && clientNum != localClientNum ) {
 		msg.Init( msgBuf, sizeof( msgBuf ) );
@@ -1391,15 +1413,190 @@
 	if (clients[clientNum].channel.GetRemoteAddress().type == NA_BOT)
 		return;
 // jmarshall end
+	serverClient_t &client = clients[ clientNum ];
+	if ( IsServerDemoPlaying() ) {
+		const int projectedSize = client.channel.GetPendingReliableBytes() + msg.GetSize() + 6;
+		if ( client.replayReliableOffset < client.replayReliableBacklog.Num() ||
+			projectedSize > SERVER_DEMO_RELIABLE_BATCH_LIMIT ) {
+			if ( QueueReplayReliableMessage( clientNum, msg ) ) {
+				return;
+			}
+			common->Warning( "replay reliable backlog exceeded %d bytes for viewer %d",
+				SERVER_DEMO_RELIABLE_BACKLOG_LIMIT, clientNum );
+			ClearReplayReliableBacklog( clientNum );
+			DropClient( clientNum, "#str_07136" );
+			return;
+		}
+		if ( client.channel.SendReliableMessage( msg ) ) {
+			if ( projectedSize > SERVER_DEMO_RELIABLE_SNAPSHOT_LIMIT ) {
+				client.replayReliableBarrier = true;
+			}
+			if ( client.replayReliableBarrier && client.replayReliableBlockedTime == 0 ) {
+				client.replayReliableBlockedTime = serverTime;
+			}
+			return;
+		}
+		if ( QueueReplayReliableMessage( clientNum, msg ) ) {
+			return;
+		}
+		common->Warning( "failed to queue reliable replay state for viewer %d", clientNum );
+		ClearReplayReliableBacklog( clientNum );
+		DropClient( clientNum, "#str_07136" );
+		return;
+	}
 
-	if ( !clients[ clientNum ].channel.SendReliableMessage( msg ) ) {
-		clients[ clientNum ].channel.ClearReliableMessages();
+	if ( !client.channel.SendReliableMessage( msg ) ) {
+		client.channel.ClearReliableMessages();
 		DropClient( clientNum, "#str_07136" );
 	}
 }
 
 /*
 ==================
+idAsyncServer::ClearReplayReliableBacklog
+==================
+*/
+void idAsyncServer::ClearReplayReliableBacklog( int clientNum ) {
+	serverClient_t &client = clients[ clientNum ];
+	client.replayReliableBacklog.Clear();
+	client.replayReliableOffset = 0;
+	client.replayReliableBlockedTime = 0;
+	client.replayReliableBarrier = false;
+}
+
+/*
+==================
+idAsyncServer::QueueReplayReliableMessage
+==================
+*/
+bool idAsyncServer::QueueReplayReliableMessage( int clientNum, const idBitMsg &msg ) {
+	serverClient_t &client = clients[ clientNum ];
+	const int entrySize = sizeof( int ) + msg.GetSize();
+	if ( msg.GetSize() <= 0 || entrySize > SERVER_DEMO_RELIABLE_BACKLOG_LIMIT ) {
+		return false;
+	}
+
+	if ( client.replayReliableBacklog.Num() + entrySize > SERVER_DEMO_RELIABLE_BACKLOG_LIMIT &&
+		client.replayReliableOffset > 0 ) {
+		const int remaining = client.replayReliableBacklog.Num() - client.replayReliableOffset;
+		memmove( client.replayReliableBacklog.Ptr(),
+			client.replayReliableBacklog.Ptr() + client.replayReliableOffset, remaining );
+		client.replayReliableBacklog.SetNum( remaining, false );
+		client.replayReliableOffset = 0;
+	}
+	if ( client.replayReliableBacklog.Num() + entrySize > SERVER_DEMO_RELIABLE_BACKLOG_LIMIT ) {
+		return false;
+	}
+	if ( client.replayReliableBacklog.NumAllocated() == 0 ) {
+		client.replayReliableBacklog.SetGranularity( 4096 );
+	}
+
+	const int offset = client.replayReliableBacklog.Num();
+	client.replayReliableBacklog.AssureSize( offset + entrySize );
+	const int messageSize = msg.GetSize();
+	memcpy( client.replayReliableBacklog.Ptr() + offset, &messageSize, sizeof( messageSize ) );
+	memcpy( client.replayReliableBacklog.Ptr() + offset + sizeof( messageSize ),
+		msg.GetData(), messageSize );
+	client.replayReliableBarrier = true;
+	if ( client.replayReliableBlockedTime == 0 ) {
+		client.replayReliableBlockedTime = serverTime;
+	}
+	return true;
+}
+
+/*
+==================
+idAsyncServer::FlushReplayReliableBacklog
+==================
+*/
+bool idAsyncServer::FlushReplayReliableBacklog( int clientNum ) {
+	serverClient_t &client = clients[ clientNum ];
+	if ( client.channel.GetPendingReliableBytes() != 0 ) {
+		return true;
+	}
+
+	while ( client.replayReliableOffset < client.replayReliableBacklog.Num() ) {
+		const int remaining = client.replayReliableBacklog.Num() - client.replayReliableOffset;
+		if ( remaining < static_cast<int>( sizeof( int ) ) ) {
+			return false;
+		}
+		int messageSize = 0;
+		memcpy( &messageSize, client.replayReliableBacklog.Ptr() + client.replayReliableOffset,
+			sizeof( messageSize ) );
+		if ( messageSize <= 0 || messageSize > MAX_MESSAGE_SIZE ||
+			messageSize + static_cast<int>( sizeof( int ) ) > remaining ) {
+			return false;
+		}
+		if ( client.channel.GetPendingReliableBytes() + messageSize + 6 >
+			SERVER_DEMO_RELIABLE_BATCH_LIMIT ) {
+			if ( client.channel.GetPendingReliableBytes() == 0 ) {
+				return false;
+			}
+			break;
+		}
+
+		idBitMsg queued;
+		queued.Init( client.replayReliableBacklog.Ptr() + client.replayReliableOffset + sizeof( int ),
+			messageSize );
+		queued.SetSize( messageSize );
+		if ( !client.channel.SendReliableMessage( queued ) ) {
+			return false;
+		}
+		client.replayReliableOffset += sizeof( int ) + messageSize;
+	}
+
+	if ( client.replayReliableOffset == client.replayReliableBacklog.Num() ) {
+		client.replayReliableBacklog.Clear();
+		client.replayReliableOffset = 0;
+	}
+	if ( client.channel.GetPendingReliableBytes() > 0 ) {
+		client.replayReliableBlockedTime = serverTime;
+	}
+	return true;
+}
+
+/*
+==================
+idAsyncServer::ReplayClientHasPendingReliable
+==================
+*/
+bool idAsyncServer::ReplayClientHasPendingReliable( int clientNum ) const {
+	const serverClient_t &client = clients[ clientNum ];
+	return client.channel.GetPendingReliableBytes() > 0 ||
+		client.replayReliableOffset < client.replayReliableBacklog.Num();
+}
+
+/*
+==================
+idAsyncServer::RefreshReplayReliableBarriers
+==================
+*/
+void idAsyncServer::RefreshReplayReliableBarriers( void ) {
+	for ( int i = 0; i < MAX_ASYNC_CLIENTS; ++i ) {
+		serverClient_t &client = clients[ i ];
+		if ( client.replayReliableBarrier && !ReplayClientHasPendingReliable( i ) ) {
+			client.replayReliableBarrier = false;
+			client.replayReliableBlockedTime = 0;
+		}
+	}
+}
+
+/*
+==================
+idAsyncServer::HasReadyReplayViewer
+==================
+*/
+bool idAsyncServer::HasReadyReplayViewer( void ) const {
+	for ( int i = 0; i < MAX_ASYNC_CLIENTS; ++i ) {
+		if ( clients[ i ].clientState == SCS_INGAME && !clients[ i ].replayReliableBarrier ) {
+			return true;
+		}
+	}
+	return false;
+}
+
+/*
+==================
 idAsyncServer::CheckClientTimeouts
 ==================
 */
@@ -1829,6 +2026,16 @@
 	if ( serverTime - client.lastSnapshotTime < idAsyncNetwork::serverSnapshotDelay.GetInteger() ) {
 		return false;
 	}
+	const int pendingReliableBytes = client.channel.GetPendingReliableBytes();
+	if ( pendingReliableBytes > 0 ) {
+		client.replayReliableBarrier = true;
+		if ( client.replayReliableBlockedTime == 0 ) {
+			client.replayReliableBlockedTime = serverTime;
+		}
+		common->DPrintf( "deferring replay snapshot until %d reliable bytes are acknowledged for viewer %d\n",
+			pendingReliableBytes, clientNum );
+		return false;
+	}
 	client.clientAheadTime = client.gameTime - gameTime;
 	memset( clientInPVS, 0, sizeof( clientInPVS ) );
 
@@ -1843,8 +2050,17 @@
 	game->RepeaterWriteSnapshot( clientNum, client.snapshotSequence, msg, clientInPVS,
 		MAX_ASYNC_CLIENTS, serverDemoViewerOrigins[clientNum], 0 );
 	msg.WriteByte( MAX_ASYNC_CLIENTS );
+	if ( !client.channel.CanSendMessage( msg.GetSize() ) ) {
+		common->Warning( "replay snapshot of %d bytes exceeds the message limit for viewer %d",
+			msg.GetSize(), clientNum );
+		DropClient( clientNum, "#str_07136" );
+		return false;
+	}
 
-	client.channel.SendMessage( serverPort, serverTime, msg );
+	if ( client.channel.SendMessage( serverPort, serverTime, msg ) < 0 ) {
+		DropClient( clientNum, "#str_07136" );
+		return false;
+	}
 	client.lastSnapshotTime = serverTime;
 	client.snapshotSequence++;
 	client.numDuplicatedUsercmds = 0;
@@ -1904,6 +2120,10 @@
 
 		// the client is in the right game
 		client.clientState = SCS_INGAME;
+		if ( IsServerDemoPlaying() ) {
+			client.replayReliableBarrier = true;
+			client.replayReliableBlockedTime = serverTime;
+		}
 
 		// send the user info of other clients
 		for ( i = 0; i < MAX_ASYNC_CLIENTS; i++ ) {
@@ -2396,7 +2616,7 @@
 	msg.ReadString( password, sizeof( password ) );
 	char reason[MAX_STRING_CHARS];
 	allowReply_t reply = IsServerDemoPlaying()
-		? game->RepeaterAllowClient( clientId, numClientsForAdmission, Sys_NetAdrToString( from ), guid, true, password, password, reason )
+		? game->RepeaterAllowClient( clientId, numClientsForAdmission, Sys_NetAdrToString( from ), guid, false, password, password, reason )
 		: game->ServerAllowClient( clientId, numClientsForAdmission, Sys_NetAdrToString( from ), guid, password, password, reason );
 	if ( reply != ALLOW_YES ) {
 		common->DPrintf( "game denied connection for %s\n", Sys_NetAdrToString( from ) );
@@ -3221,6 +3441,15 @@
 		if ( idAsyncNetwork::serverMaxClientRate.IsModified() ) {
 			client.channel.SetMaxOutgoingRate( Min( client.clientRate, idAsyncNetwork::serverMaxClientRate.GetInteger() ) );
 		}
+		if ( IsServerDemoPlaying() && client.replayReliableBarrier &&
+			ReplayClientHasPendingReliable( i ) && client.clientState == SCS_INGAME &&
+			client.replayReliableBlockedTime > 0 &&
+			serverTime - client.replayReliableBlockedTime > SERVER_DEMO_RELIABLE_STALL_MSEC ) {
+			common->Warning( "replay viewer %d did not acknowledge reliable state for %d msec",
+				i, SERVER_DEMO_RELIABLE_STALL_MSEC );
+			DropClient( i, "#str_07136" );
+			continue;
+		}
 
 		// if the channel is not yet ready to send new data
 		if ( !client.channel.ReadyToSend( serverTime ) ) {
@@ -3233,6 +3462,22 @@
 			continue;
 		}
 
+		if ( IsServerDemoPlaying() && client.replayReliableBarrier ) {
+			if ( !ReplayClientHasPendingReliable( i ) ) {
+				client.replayReliableBarrier = false;
+				client.replayReliableBlockedTime = 0;
+			} else if ( client.channel.GetPendingReliableBytes() == 0 &&
+				!FlushReplayReliableBacklog( i ) ) {
+				common->Warning( "invalid or oversized replay reliable backlog for viewer %d", i );
+				DropClient( i, "#str_07136" );
+				continue;
+			}
+			if ( client.replayReliableBarrier ) {
+				SendEmptyToClient( i, true );
+				continue;
+			}
+		}
+
 		if ( client.clientState == SCS_INGAME ) {
 			const bool sentSnapshot = IsServerDemoPlaying()
 				? SendRepeaterSnapshotToClient( i )
