Help with broadcasting a message to all the users (NotificationSendAll or Custom)

Hello!,

I have some custom behaviour for leaderboard with a reset schedule. They reset weekly and I have some custom behaviour to reward the top users. I also need to notify every other user about the week being reset.

I was wondering on using the NakamaModule’s NotificationSendAll , but I don’t know details of its implementation, the game is likely to have about a million users registered on the database. Not being a server programmer myself, I was wondering if this acceptable or if that would be a problem on production with that amount of user.
The last resource would be handling it locally using the leaderboard reset time. But this is not the ideal solution since I’ll have to handle quite a few edge cases that wouldn’t be a problem if the server fires a message when the week ends.

Another related issue it’s deleting existing notifications with that ID every week (to avoid users stacking the same reset notification). I implemented a reset function but I also wonder if it would be too much stress on the server.

  1. Versions: Nakama 3.30
  2. Server Framework Runtime language Go

Reseting and broadcasting the notification:

content := map[string]interface{}{"leaderboardId": targetID}

s.ClearUserNotificationsByCode(ctx, db, logger, CodeLeaderboardReset)

nk.NotificationSendAll(ctx, "Weekly Reset", content, CodeLeaderboardReset, true)

Implemented this function to clear the notifications


func (s *CustomLeaderboard) ClearUserNotificationsByCode(ctx context.Context, db *sql.DB, logger runtime.Logger, code int) error {
	query := `
		DELETE FROM notification 
		WHERE id IN (
			SELECT id FROM notification 
			WHERE code = $1 
			LIMIT 5000
		)
	`
	for {
		result, err := db.ExecContext(ctx, query, code)
		if err != nil {
			logger.Error("Failed to batch delete notifications: %v", err)
			return err
		}
		rows, _ := result.RowsAffected()
		if rows == 0 {
			break
		}
	}
	return nil
}

And this is the alternative broadcast function with Batching I thought on using instead of nk.NotificationSendAll, doing batches of 5000 users

// Broacasting

func (s *CustomLeaderboard) BroadcastResetNotification(ctx context.Context, db *sql.DB, logger runtime.Logger, sourceID string) error {
	// 1. Clear old reset notifications
	if err := s.ClearUserNotificationsByCode(ctx, db, logger, CodeLeaderboardReset); err != nil {
		return err // Stop if we can't clear old data to avoid massive stacking
	}

	content, err := json.Marshal(map[string]interface{}{"id": sourceID})
	if err != nil {
		return err
	}

	query := `
		INSERT INTO notification (id, user_id, subject, content, code, sender_id, create_time, persistent)
		SELECT gen_random_uuid(), u.id, 'Season Reset', $1, $2, '', (now() AT TIME ZONE 'UTC'), true
		FROM users u
		WHERE u.disable_time = '1970-01-01 00:00:00+00'
		  AND NOT EXISTS (
			  SELECT 1 FROM notification n 
			  WHERE n.user_id = u.id AND n.code = $2
		  )
		LIMIT 5000
	`

	totalBroadcasted := int64(0)
	for {
		batchCtx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
		result, err := db.ExecContext(batchCtx, query, string(content), CodeQueensLeaderboardReset)
		cancel()

		if err != nil {
			logger.Error("SQL Broadcast batch failed after %d users: %v", totalBroadcasted, err)
			return err
		}

		rows, _ := result.RowsAffected()
		if rows == 0 {
			break
		}

		totalBroadcasted += rows
		time.Sleep(20 * time.Millisecond)
	}

	logger.Info("Global broadcast complete. Sent %d notifications.", totalBroadcasted)
	return nil
}

Hi @Manuel_Martin,

Typically we’d avoid notifications that have to go through the entire set of players - the NotificationsSendAll API already batches writes internally, but it’s only meant to be used for very specific cases where an alternative isn’t possible.

For the use-case you describe, a better approach would be to have the client list any active leaderboards and cache their expiry, and possibly schedule a local notification for when the next expiry happens, or something along those lines. This way offload this computation to the client and spread the load across requests, instead of having a centralized processor that has to go through every player every reset cycle.

As for the reward notifications, in Hiro we avoid this tournament reset/end pattern for precisely the same reason, the better approach is to notify the user that a leaderboard/tournament they participated in has reset/completed, and surface a button that allows them to claim the reward, and computing any reward granting at that point in time. This way, we skip processing for inactive players and spread the computations in time, avoiding CPU spikes on every reset.

If you’re rewarding a subset of the top X ranks of a leaderboard, it’s okay to use NotificationsSend to batch these notifications, as long as it’s a relatively small subset of the player base.

Hope this clarifies.