Review counts and ratings updated near-real-time on submission
Why it matters
Rating counts and averages that update on a nightly batch job rather than in real time allow the displayed schema-org aggregateRating to diverge from the database truth for hours after a review is submitted. A listing that receives 10 new 1-star reviews after a service failure continues displaying its pre-incident 4.5-star average until the batch runs — actively misleading users making decisions based on the structured data. Data-integrity is the core taxon: the JSON-LD reviewCount fed to Google must match the actual review count at page-render time. Delayed updates also suppress Google's trust in your rating freshness, reducing the likelihood of rich-result display.
Severity rationale
High because batch-delayed rating updates allow structured data to misrepresent actual listing quality for hours, creating a data-integrity gap that Google's structured data guidelines prohibit.
Remediation
Recalculate averageRating and reviewCount synchronously in the same transaction that writes the review. Avoid recomputing from a full table scan on every submit — maintain running aggregates on the listing row.
// src/app/api/listings/[id]/reviews/route.ts
export async function POST(request: Request, { params }: { params: { id: string } }) {
const { rating, text } = await request.json();
await db.$transaction(async (tx) => {
await tx.review.create({ data: { listingId: params.id, rating, text } });
// Recompute aggregate from all reviews in the same transaction
const { _avg, _count } = await tx.review.aggregate({
where: { listingId: params.id },
_avg: { rating: true },
_count: { rating: true },
});
await tx.listing.update({
where: { id: params.id },
data: {
averageRating: parseFloat((_avg.rating ?? 0).toFixed(2)),
reviewCount: _count.rating,
dateModified: new Date(),
},
});
});
return Response.json({ ok: true });
}
The dateModified update in the same transaction ensures the JSON-LD timestamp stays current.
Detection
-
ID:
rating-freshness -
Severity:
high -
What to look for: Count all listings with ratings. For each, find the review submission API or logic. After a new review is submitted, check whether the listing's
averageRatingandreviewCountare immediately recalculated and persisted. Look for cache invalidation or real-time updates. -
Pass criteria: Review counts and ratings are recalculated within seconds of a new review being submitted (or approved) — ratings older than 6 months must be flagged or weighted differently in the aggregate score. Report even on pass: "X rated listings checked, Y have ratings older than 6 months."
-
Fail criteria: Ratings are updated only on a schedule (e.g., hourly) or not updated until the next page refresh.
-
Skip (N/A) when: Listings don't have reviews.
-
Detail on fail: Example:
"Review submitted but reviewCount stays same for 1 hour until batch job runs"or"New review changes rating but listing page doesn't reflect it until next visit" -
Remediation: Recalculate ratings immediately on review submission:
export async function POST(request) { const { listingId, rating, text } = await request.json() const review = await db.review.create({ data: { listingId, rating, text } }) const reviews = await db.review.findMany({ where: { listingId } }) const avgRating = reviews.reduce((sum, r) => sum + r.rating, 0) / reviews.length const listing = await db.listing.update({ where: { id: listingId }, data: { averageRating: parseFloat(avgRating.toFixed(2)), reviewCount: reviews.length, dateModified: new Date() } }) return NextResponse.json(listing) }
External references
- schema-org · AggregateRating — AggregateRating must reflect near-real-time review submissions
- schema-org · reviewCount — reviewCount must update within seconds of new review approval
Taxons
History
- 2026-04-18·v1.0.0·Initial import from directory-listing-schema·automated