diff --git a/src/sys/emscripten/wasm_net.cpp b/src/sys/emscripten/wasm_net.cpp
--- a/src/sys/emscripten/wasm_net.cpp
+++ b/src/sys/emscripten/wasm_net.cpp
@@ -18,6 +18,7 @@
 #include <ctype.h>
 #include <emscripten/emscripten.h>
 #include <emscripten/websocket.h>
+#include <limits.h>
 #include <stdint.h>
 #include <stdlib.h>
 
@@ -34,6 +35,7 @@
 const size_t WEB_BACKPRESSURE_BYTES = 512 * 1024;
 const double WEB_RECONNECT_MIN_MSEC = 250.0;
 const double WEB_RECONNECT_MAX_MSEC = 10000.0;
+const double WEB_NOTICE_INTERVAL_MSEC = 1000.0;
 
 struct webDatagram_t {
 	netadr_t	from;
@@ -130,13 +132,23 @@ struct webPortState_t {
 	double				retryAt;
 	unsigned int		droppedIncoming;
 	unsigned int		droppedOutgoing;
+	unsigned int		pendingConnectedNotices;
+	unsigned int		pendingErrorNotices;
+	unsigned int		pendingCloseNotices;
+	unsigned int		pendingIncomingDrops;
+	unsigned int		lastCloseCode;
+	char				lastCloseReason[ 128 ];
+	double				nextNoticeAt;
 	idWebDatagramQueue incoming;
 	idWebDatagramQueue outgoing;
 
 	webPortState_t() : owner( NULL ), socket( 0 ), phase( WEB_SOCKET_IDLE ),
 			hasRemote( false ), closing( false ), retryAttempt( 0 ), retryAt( 0.0 ),
-			droppedIncoming( 0 ), droppedOutgoing( 0 ) {
+			droppedIncoming( 0 ), droppedOutgoing( 0 ), pendingConnectedNotices( 0 ),
+			pendingErrorNotices( 0 ), pendingCloseNotices( 0 ), pendingIncomingDrops( 0 ),
+			lastCloseCode( 0 ), nextNoticeAt( 0.0 ) {
 		memset( &remote, 0, sizeof( remote ) );
+		memset( lastCloseReason, 0, sizeof( lastCloseReason ) );
 	}
 };
 
@@ -231,6 +243,56 @@ static double Web_Now() {
 	return emscripten_get_now();
 }
 
+static void Web_IncrementBounded( unsigned int &counter ) {
+	if ( counter < UINT_MAX ) {
+		counter++;
+	}
+}
+
+static void Web_ReportPending( webPortState_t *state ) {
+	if ( state == NULL || ( state->pendingConnectedNotices == 0 && state->pendingErrorNotices == 0 &&
+			state->pendingCloseNotices == 0 && state->pendingIncomingDrops == 0 ) ) {
+		return;
+	}
+
+	const double now = Web_Now();
+	if ( now < state->nextNoticeAt ) {
+		return;
+	}
+
+	const unsigned int connected = state->pendingConnectedNotices;
+	const unsigned int errors = state->pendingErrorNotices;
+	const unsigned int closes = state->pendingCloseNotices;
+	const unsigned int dropped = state->pendingIncomingDrops;
+	const unsigned int totalDropped = state->droppedIncoming;
+	const unsigned int closeCode = state->lastCloseCode;
+	char closeReason[ sizeof( state->lastCloseReason ) ];
+	idStr::Copynz( closeReason, state->lastCloseReason, sizeof( closeReason ) );
+
+	// Clear before reporting because common output can pump a loading frame.
+	state->pendingConnectedNotices = 0;
+	state->pendingErrorNotices = 0;
+	state->pendingCloseNotices = 0;
+	state->pendingIncomingDrops = 0;
+	state->nextNoticeAt = now + WEB_NOTICE_INTERVAL_MSEC;
+
+	if ( connected > 0 ) {
+		common->Printf( "WebSocket UDP gateway connected: %s (%u event(s))\n",
+				Sys_NetAdrToString( state->remote ), connected );
+	}
+	if ( errors > 0 ) {
+		common->DPrintf( "WebSocket UDP gateway error; reconnect scheduled (%u event(s))\n", errors );
+	}
+	if ( closes > 0 ) {
+		common->DPrintf( "WebSocket UDP gateway closed (%u: %s); reconnect scheduled (%u event(s))\n",
+				closeCode, closeReason, closes );
+	}
+	if ( dropped > 0 ) {
+		common->Warning( "WebSocket UDP gateway dropped %u incoming frame(s) (%u total)",
+				dropped, totalDropped );
+	}
+}
+
 static void Web_DeleteSocket( webPortState_t *state, bool sendClose ) {
 	if ( state == NULL || state->socket <= 0 ) {
 		return;
@@ -271,7 +333,7 @@ static bool Web_OnOpen( int, const EmscriptenWebSocketOpenEvent *event, void *us
 	state->phase = WEB_SOCKET_OPEN;
 	state->retryAttempt = 0;
 	state->retryAt = 0.0;
-	common->Printf( "WebSocket UDP gateway connected: %s\n", Sys_NetAdrToString( state->remote ) );
+	Web_IncrementBounded( state->pendingConnectedNotices );
 	Web_FlushOutgoing( state );
 	return true;
 }
@@ -287,10 +349,8 @@ static bool Web_OnMessage( int, const EmscriptenWebSocketMessageEvent *event, voi
 	}
 	if ( event->numBytes > WEB_MAX_DATAGRAM_BYTES ||
 			!state->incoming.Push( state->remote, event->data, static_cast<int>( event->numBytes ) ) ) {
-		state->droppedIncoming++;
-		if ( state->droppedIncoming == 1 || ( state->droppedIncoming & 63 ) == 0 ) {
-			common->Warning( "WebSocket UDP gateway dropped %u incoming frame(s)", state->droppedIncoming );
-		}
+		Web_IncrementBounded( state->droppedIncoming );
+		Web_IncrementBounded( state->pendingIncomingDrops );
 	}
 	return true;
 }
@@ -300,7 +360,7 @@ static bool Web_OnError( int, const EmscriptenWebSocketErrorEvent *event, void *u
 	if ( state == NULL || state->closing || state->socket != event->socket ) {
 		return true;
 	}
-	common->DPrintf( "WebSocket UDP gateway error; reconnect scheduled\n" );
+	Web_IncrementBounded( state->pendingErrorNotices );
 	Web_DeleteSocket( state, true );
 	Web_ScheduleReconnect( state );
 	return true;
@@ -311,8 +371,9 @@ static bool Web_OnClose( int, const EmscriptenWebSocketCloseEvent *event, void *u
 	if ( state == NULL || state->closing || state->socket != event->socket ) {
 		return true;
 	}
-	common->DPrintf( "WebSocket UDP gateway closed (%u: %s); reconnect scheduled\n",
-			static_cast<unsigned int>( event->code ), event->reason );
+	Web_IncrementBounded( state->pendingCloseNotices );
+	state->lastCloseCode = static_cast<unsigned int>( event->code );
+	idStr::Copynz( state->lastCloseReason, event->reason, sizeof( state->lastCloseReason ) );
 	Web_DeleteSocket( state, false );
 	Web_ScheduleReconnect( state );
 	return true;
@@ -624,6 +685,7 @@ bool idPort::GetPacket( netadr_t &from, void *data, int &size, int maxSize ) {
 		return false;
 	}
 	webPortState_t *state = reinterpret_cast<webPortState_t *>( static_cast<intptr_t>( netSocket ) );
+	Web_ReportPending( state );
 	Web_EnsureSocket( state );
 	Web_FlushOutgoing( state );
 	const webDatagram_t *entry = state->incoming.Front();
