javascriptreactjsnext.jscronvercel

Date returns the same value across multiple requests when deployed to Vercel (Next.js Route Handler)


I'm using a Next.js App Router API route (app/api/notify/route.ts) to check the current time (in Brazil timezone) and trigger notifications when a scheduled time is reached.

This works locally, but after deploying to Vercel, I noticed unexpected behavior:

❌ Problem: After the first request, all subsequent requests return the same timestamp (frozen time from the first execution), even though I'm using new Date() in the handler.

It looks like the function is being cached or the environment is reusing the same context.

✅ Expected Behavior: Every time the route is hit, new Date() should return the current time, reflecting the actual moment of the request.

✅ Reproducible Steps: Deploy this simple handler to Vercel.

Send multiple GET requests to the endpoint with a few seconds/minutes of delay.

You'll notice the timestamp and isoTime never change after the first call.

import connectMongo from "@/libs/mongoose";
import Routine from "@/models/Routine";
import User from "@/models/User";
import PushSubscription from "@/models/PushSubscription";
const webPush = require("web-push");

const vapidPublicKey = process.env.NEXT_PUBLIC_WEB_PUSH_PUBLIC_KEY;
const vapidPrivateKey = process.env.WEB_PUSH_PRIVATE_KEY;

if (!vapidPublicKey || !vapidPrivateKey) {
    throw new Error("VAPID keys not configured");
}

console.log("VAPID keys configured correctly");
webPush.setVapidDetails("mailto:your.email@example.com", vapidPublicKey, vapidPrivateKey);

const getBrazilDateTime = () => {
    const now = new Date();
    const brazilTime = new Date(now.getTime() - 3 * 60 * 60 * 1000); // adjust for Brazil timezone
    return brazilTime;
};

const getDayNameInPortuguese = (date) => {
    const days = ["Domingo", "Segunda", "Terça", "Quarta", "Quinta", "Sexta", "Sábado"];
    return days[date.getDay()];
};

const getTaskStartTime = (task, currentDay) => {
    if (task.dailySchedule instanceof Map && task.dailySchedule.has(currentDay)) {
        const daySchedule = task.dailySchedule.get(currentDay);
        if (daySchedule && daySchedule.startTime) return daySchedule.startTime;
    } else if (task.dailySchedule && typeof task.dailySchedule === "object" && task.dailySchedule[currentDay]?.startTime) {
        return task.dailySchedule[currentDay].startTime;
    }
    if (task.startTime) return task.startTime;
    return null;
};

const deduplicateSubscriptions = (subscriptions) => {
    const uniqueEndpoints = new Set();
    return subscriptions.filter((sub) => {
        if (uniqueEndpoints.has(sub.endpoint)) return false;
        uniqueEndpoints.add(sub.endpoint);
        return true;
    });
};

const notifiedTasksCache = new Map();

const isTaskAlreadyNotified = (taskId, userId) => {
    const key = `${taskId}-${userId}`;
    const lastNotified = notifiedTasksCache.get(key);
    if (!lastNotified) return false;
    const tenMinutesAgo = Date.now() - 10 * 60 * 1000;
    return lastNotified > tenMinutesAgo;
};

const markTaskAsNotified = (taskId, userId) => {
    const key = `${taskId}-${userId}`;
    notifiedTasksCache.set(key, Date.now());
};

export async function GET(request) {
    const headers = new Headers({
        "Cache-Control": "no-store, max-age=0, must-revalidate",
        "Content-Type": "application/json",
    });

    const logs = [];
    const addLog = (message) => {
        console.log(message);
        logs.push(`[${new Date().toISOString()}] ${message}`);
    };

    addLog("🔔 Starting notification check...");
    addLog(`🕒 Start timestamp: ${Date.now()}`);

    try {
        addLog("Connecting to MongoDB...");
        await connectMongo();
        addLog("MongoDB connection established");

        // Update the time on each cycle to ensure the time is current.
        const spDate = getBrazilDateTime(); 
        const currentDay = getDayNameInPortuguese(spDate);
        const currentTime = spDate.toLocaleTimeString("pt-BR", {
            hour: "2-digit",
            minute: "2-digit",
            hour12: false,
        });

        addLog(`📅 Brazil date and time: ${spDate.toLocaleString("pt-BR")}`);
        addLog(`📅 Day of the week: ${currentDay}`);
        addLog(`⏰ Current time: ${currentTime}`);

        const users = await User.find({ activeRoutine: { $exists: true, $ne: null } });
        addLog(`👥 Found ${users.length} users with active routines`);

        if (!users.length) return NextResponse.json({ message: "No users with active routines found." });

        const routineIds = users.map((user) => user.activeRoutine).filter(Boolean);
        const routines = await Routine.find({ _id: { $in: routineIds } });
        const routineMap = new Map();
        routines.forEach((routine) => routineMap.set(routine._id.toString(), routine));

        let notificationsSent = 0;
        let usersNotified = 0;
        let duplicatesSkipped = 0;

        await Promise.all(
            users.map(async (user) => {
                const routineId = user.activeRoutine?.toString();
                if (!routineId) return;
                const routine = routineMap.get(routineId);
                if (!routine) return;

                const matchingTasks = routine.tasks.filter((task) => {
                    const taskDays = task.days || [];
                    const includesDay = taskDays.includes(currentDay);
                    const taskStartTime = getTaskStartTime(task, currentDay);
                    return includesDay && taskStartTime === currentTime;
                });

                if (!matchingTasks.length) return;

                for (const matchingTask of matchingTasks) {
                    if (isTaskAlreadyNotified(matchingTask._id.toString(), user._id.toString())) {
                        duplicatesSkipped++;
                        continue;
                    }

                    let subscriptions = await PushSubscription.find({ userId: user._id });
                    if (!subscriptions.length) continue;

                    subscriptions = deduplicateSubscriptions(subscriptions);
                    addLog(`  📱 User ${user.email} has ${subscriptions.length} unique devices`);

                    const payload = JSON.stringify({
                        title: `🔔 ${matchingTask.name} - Time to start!`,
                        body: `⏰ ${currentTime} - ${matchingTask.details || "Stay focused on your routine!"}`,
                        icon: "/icon512_rounded.png",
                        badge: "/icon192_rounded.png",
                        tag: `task-${matchingTask._id}`,
                        data: {
                            url: `/dashboard/r/${routine._id}`,
                            taskId: matchingTask._id.toString(),
                            type: "task-reminder",
                            timestamp: new Date().toISOString(),
                        },
                        actions: [
                            { action: "open", title: "📋 View Details" },
                            { action: "dismiss", title: "✔️ Got it" },
                        ],
                        vibrate: [200, 100, 200],
                        requireInteraction: true,
                    });

                    await Promise.all(
                        subscriptions.map(async (subscription) => {
                            try {
                                await webPush.sendNotification(
                                    {
                                        endpoint: subscription.endpoint,
                                        keys: subscription.keys,
                                    },
                                    payload
                                );
                                notificationsSent++;
                                markTaskAsNotified(matchingTask._id.toString(), user._id.toString());
                            } catch (error) {
                                if (error.statusCode === 410) {
                                    await PushSubscription.deleteOne({ _id: subscription._id });
                                }
                            }
                        })
                    );
                    usersNotified++;
                }
            })
        );

        return NextResponse.json(
            {
                message: `Notifications sent successfully!`,
                notificationsSent,
                usersNotified,
                duplicatesSkipped,
                logs,
            },
            { headers }
        );
    } catch (error) {
        console.error("Error sending notifications:", error);
        return NextResponse.json({ error: "Error processing notifications.", logs }, { status: 500, headers });
    }
}

🔍 What I’ve tried: Removed all external logic — minimal handler above still reproduces the issue.

Vercel cache control headers: doesn't seem related.

Checked if global or any static variable is retaining state — not the case.

❓ My Question: Is this a known issue with Vercel’s function caching or cold start behavior in Next.js App Router?

How can I ensure that new Date() is evaluated on every request, not just on the first one?


Solution

  • The primary issue is with function instances being reused in Vercel's serverless environment. Here's how to fix it:

    import { NextResponse } from "next/server";
    

    Fix the date handling: The main problem is that even though you're calling new Date() inside your handler, other parts of your code might be cached.

     const getBrazilDateTime = () => {
      // Force a fresh Date object on every call
      const now = new Date(); 
      // Convert to Brazil timezone (UTC-3)
      return new Intl.DateTimeFormat('pt-BR', {
        timeZone: 'America/Sao_Paulo',
        year: 'numeric',
        month: 'numeric',
        day: 'numeric',
        hour: 'numeric',
        minute: 'numeric',
        second: 'numeric',
        hour12: false
      }).format(now);
    };
    

    Add a force reload flag to ensure functions aren't reused:

    export const dynamic = 'force-dynamic';
    export const revalidate = 0;
    

    or with proper timezone handling: Instead of manually adjusting the time by subtracting 3 hours, use the Intl API for proper timezone handling:

    const getBrazilDateTime = () => {
      const now = new Date();
      // Using proper timezone conversion
      const formatter = new Intl.DateTimeFormat('pt-BR', {
        timeZone: 'America/Sao_Paulo',
      });
      return new Date(formatter.format(now));
    };