To lecimy...
Gdy user się loguje sprawdzasz kiedy był ostatnio na forum (akcje typu dodaj post, czytanie tematu, przechodzenie między podforami itp... cokolwiek w strukturze forum). Jeśli nie było nigdy walisz mu ideki X najnowszych postów. Jeśli był to pobierasz wszystkie posty od tej daty do aktualnej z LIMIT X i ustawiasz mu znacznik czasu akcji na forum na aktualny. Te id wrzucasz do jakiejś tabeli, gdzie kluczem jest id usera, a dodatkowo masz mieć tam id posta ( tematu czy forum opcjonalnie jeśli chcesz zmniejszyć nieco późniejsze joiny ). W ten sposób stworzysz sumę starych i nowych nieprzeczytanych. Jeśli ta suma przekroczy X, usuwasz najstarsze powyżej limitu. Jako że posty mają strukturę zazwyczaj z id postu jako auto_increment, to wystarczy że sprawdzisz jaki id ma post o offsecie X i wszystkie dla danego usera z tej tabeli o id mniejszym wyrzucasz. W ten sposób robisz przy każdej akcji wewnątrz struktury forum. Niby generuje to ruch, ale zauważ, że podczas sprawdzania ile postów jest nowych od "starej" daty można w przypadku braku odpuścić dalszą część, przez co optymalizujemy szybkość eliminując zbędne operacje.
EDIT: Przykład...
User loguje się po raz pierwszy, więc w znaczniku czasu ma zapewne NULL lub 0, czyli przy wejściu na forum do tablicy wrzuca mu 150 (limit) wersów z jego id, id_post dla najwyższych postów w tabeli postów i id topicu oraz ustawia znacznik czasu dla akcji forumowych na $teraz = now(). Zaraz potem user przechodzi na stronę z najnowszymi postami, tu procedura wygląda nieco inaczej. Sprawdzenie znacznika pokaże czas z momentu wejścia na forum, a więc szuka posty z czasem powstania większym niż $teraz. Może coś być, ale nie musi. Jeśli jest, dopisze nowe posty do tej tabeli i wykona sprawdzenie czy jest ich więcej niż 150. Jeśli tak to sprawdzi id_post dla tego na miejscu 150 i wszystkie wpisy z tej tabeli o id_usera zgodnym oraz id_post mniejszym niż ten na 150 miejscu są usuwane z niej. Tak czy inaczej, niezależnie od wyniku jakichkolwiek operacji, aktualizujesz $teraz znów na aktualną. Wejście na dany temat oznacza usunięcie postów z tabeli nieprzeczytanych. Zauważ, że masz w kilku miescach sprawdzenie, które w zależności od wyniku pokazuje jakie operacje musisz wykonać lub możesz olać. Są to:
1. Sprawdzenie ilości postów od czasu wskazywanego przez $teraz -> to decyduje czy dodasz nowe rekordy do tabeli nieprzeczytanych postów
2. Sprawdzenie czy nie przekroczono limitu 150 postów -> tu reakcją jest obcięcie nadmiarowych powyżej limitu.
Adnotacja 1: Operacje usuwania z tabeli nieprzeczytanych robimy PRZED operacją sprawdzania. Czemu? Bo wejście na temat gdy mamy tylko jeden nieprzeczytany post w tym akurat temacie usunie go i porównanie ilości wpisów w tej tabeli zwróci 0 co optymalizuje całość.
Adnotacja 2: Usuwanie postów z tematu można rozbić na 2 przypadki. Albo usuwamy posty z tabeli poprzez ogólnie odwiedzenie tematu z tym postem (szybsze i prostsze w implementacji), albo jeśli temat jest długi to dodatkowo kontrolujemy, czy post mieści się na czytanej podstronie i od tego uzależniamy czy uznajemy post za przeczytany lub nie. Trochę więcej kodu sprawdzającego przed usunięciem w 2 przypadku, bo polegającego na sprawdzeniu wartości pierwszego i ostatniego id dla podstrony i porównanie z id w tabeli nieprzeczytanych dla danego tematu (stąd sugerowałem od razu zapisać też id tematu do tej tabeli).
I najważniejsze. To sa moje własne przemyślenia jako webmastera i informatyka. NIE WIEM jak fora i ich skrypty problem rozwiązują, bo
nie patrzyłem w żaden kod od jakiegokolwiek nigdy. Ale jak dla mnie to jeden z sensowniejszych i logiczniejszych, a zarazem prostych.
EDIT2: Dobrze, że w sumie podniosłeś temat, bo mi to przyspieszyło myślenie o tym jak rowiązać ów problem pisząc własny system tego typu

Może nie jest on optymalny, ale z tego coe wiem wiele for ma problemy z wydajnością jeśli stosuje system zapamiętywania nieprzeczytanych. Choć to i tak nic w porównaniu do "żarcia zasobów" bajerów w stylu shoutboxa, które limity potrafią osiągnąć błyskawicznie.