Serving stale availability data is a direct cause of double-bookings. When a customer books slot A, that change must be visible to every other client on the next calendar load — not after a session refresh or browser restart. Client-side caches without TTLs violate CWE-362 (concurrent resource access) and ISO 25010 functional correctness: two users see the same slot as open and both confirm. The downstream effect is a support ticket, a refund, and a damaged reputation. Booking platforms live and die by schedule integrity; a single double-booking erodes trust faster than any competitor can.
Critical because stale availability displayed to the user is the direct mechanical cause of double-bookings, which are unrecoverable without manual intervention and refunds.
Add month and year (or equivalent time-period identifiers) to the useEffect dependency array in your calendar component so a fresh fetch fires on every navigation, not just on mount. Include a cache-buster parameter or set Cache-Control: no-cache on the server endpoint.
useEffect(() => {
fetch(`/api/availability?month=${month}&year=${year}&t=${Date.now()}`)
.then(r => r.json())
.then(setSlots)
}, [month, year]) // re-runs on every period change
On the API route in src/app/api/availability/route.ts, set the response header:
return new Response(JSON.stringify(slots), {
headers: { 'Cache-Control': 'no-cache, no-store, must-revalidate' },
})
ID: booking-calendar-availability.calendar-display.data-freshness
Severity: critical
What to look for: Find the calendar component and trace its data loading logic. Count all calendar view components (month view, week view, day view). For each, check if there's an API call (fetch, axios, useSWR, useQuery, or HTTP client) that fires when the calendar mounts or when the user navigates to a different month/week. Look for caching mechanisms — if the code caches availability data without a time-based expiry or cache-buster, flag it. Check if the availability endpoint has a Cache-Control header set to no-cache or max-age=0. Examine files matching src/components/*calendar*, src/components/*Calendar*, src/app/*booking*, src/app/*calendar*, src/app/api/availability*.
Pass criteria: Count all calendar view mount/navigation paths. 100% of calendar view loads (mount or navigation to a different time period) must trigger a fresh API call to the availability endpoint. No stale cached data may be displayed. The availability fetch must include either a cache-buster parameter (e.g., Date.now()) or proper Cache-Control: no-cache headers. Report: "X of Y calendar view paths fetch fresh data from the server."
Fail criteria: Availability data is cached indefinitely on the client side, or the API is called only once per session and data is reused even after bookings may have changed.
Do NOT pass when: A useEffect or data-fetching hook exists but its dependency array is empty ([]) — this means it fires only on mount, not on navigation between time periods. Also do NOT pass when staleTime: Infinity or equivalent infinite caching is configured in React Query / SWR.
Skip (N/A) when: Never — calendar availability must always be current.
Detail on fail: Example: "Calendar mounts with a static list of slots fetched once at app init. Subsequent calendar views reuse the same data without refreshing. Booked slots remain marked available."
Cross-reference: Check buffer-enforcement — stale data combined with missing buffer validation causes double-bookings.
Cross-reference: Check live-availability-update — if availability is not refetched on view load, it is also likely stale after booking.
Cross-reference: Check performance-load-time — fresh fetches should not block first paint; use Suspense or skeleton.
Remediation: Fetch availability fresh on every calendar load or view change:
import { useEffect, useState } from 'react'
export function Calendar({ month, year }) {
const [slots, setSlots] = useState([])
useEffect(() => {
// Fetch whenever month/year changes
const fetchSlots = async () => {
const response = await fetch(
`/api/availability?month=${month}&year=${year}&cache-bust=${Date.now()}`
)
const data = await response.json()
setSlots(data)
}
fetchSlots()
}, [month, year])
return (
<div>
{slots.map((slot) => (
<div key={slot.id}>{slot.time}</div>
))}
</div>
)
}
Ensure the server endpoint has proper cache headers:
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate')