Vercel interpretuje harmonogram w UTC i wywołuje metodą GET wyłącznie aktualne wdrożenie produkcyjne. Nie używaj lokalnej godziny bez przeliczenia zmian czasu letniego. Cron nie podąża też za odpowiedziami 3xx, więc path musi prowadzić bezpośrednio do istniejącego handlera, bez przekierowania.
Route Handler jako adres zadania cron
Code
// app/api/cron/daily-report/route.tsimport { NextResponse } from 'next/server'import { resend } from '@/lib/resend'import { db } from '@/lib/db'export const maxDuration = 60export async function GET(request: Request) { // Weryfikacja: tylko Vercel Cron może wywołać ten adres. const authHeader = request.headers.get('authorization') const cronSecret = process.env.CRON_SECRET if (!cronSecret || authHeader !== `Bearer ${cronSecret}`) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) } const end = new Date() end.setUTCHours(0, 0, 0, 0) const start = new Date(end) start.setUTCDate(start.getUTCDate() - 1) const reportDate = start.toISOString().slice(0, 10) const stats = await db.order.aggregate({ _count: true, _sum: { total: true }, where: { createdAt: { gte: start, lt: end }, }, }) const { error } = await resend.emails.send( { from: 'NazwaFirmy <raporty@example.com>', to: 'mike@example.com', subject: `Raport dzienny: ${reportDate}`, html: ` <h2>Raport za ${reportDate}</h2> <p>Zamówienia: ${stats._count}</p> <p>Przychód: ${stats._sum.total || 0} PLN</p> `, }, // Chroni wysyłkę przed podwójnym wywołaniem tego samego dnia. { idempotencyKey: `daily-report/${reportDate}` }, ) if (error) throw new Error(`Nie udało się wysłać raportu: ${error.message}`) return NextResponse.json({ success: true, orders: stats._count, revenue: stats._sum.total, })}
W panelu Vercel przejdź do zakładki Settings, a następnie Environment Variables i dodaj zmienną CRON_SECRET. Vercel będzie automatycznie przekazywał jej wartość w nagłówku Authorization przy każdym wywołaniu zadania cron.
maxDuration jest wskazówką dla platformy wdrożeniowej, a nie sposobem na nieograniczone wykonanie. Rzeczywisty limit zależy od hostingu i planu. Vercel nie ponawia nieudanych zadań cron, może jednak uruchomić ten sam termin więcej niż raz i rozpocząć kolejne wykonanie, zanim poprzednie zdąży się zakończyć. Klucz idempotencji u dostawcy poczty elektronicznej zabezpiecza wyłącznie tę pojedynczą operację, dlatego zmiany w bazie danych należy chronić transakcjami oraz unikalnymi kluczami biznesowymi. Dodatkowo, przed równoległym uruchomieniem zadań warto zastosować blokadę rozproszoną z czasem wygaśnięcia TTL.
Upstash QStash jako bezserwerowa kolejka wiadomości
to kolejka wiadomości z HTTP API. Sprawdza się
przy zadaniach, które mają być dostarczone co najmniej raz (
), mogą być
opóźnione albo wymagają automatycznego ponawiania. „Co najmniej raz” oznacza, że
odbiorca musi tolerować duplikaty.
// actions/schedule-report.ts'use server'import { Client } from '@upstash/qstash'import { requireAdmin } from '@/lib/auth'const qstashToken = process.env.QSTASH_TOKENconst appUrl = process.env.APP_URLif (!qstashToken || !appUrl) { throw new Error('Brakuje QSTASH_TOKEN lub APP_URL')}const qstash = new Client({ token: qstashToken,})export async function scheduleWeeklyReport() { await requireAdmin() const jobId = crypto.randomUUID() const result = await qstash.publishJSON({ url: `${appUrl}/api/cron/weekly-report`, body: { jobId, type: 'weekly' }, retries: 3, // 3 ponowienia po pierwszym wywołaniu failureCallback: `${appUrl}/api/qstash/failure`, }) return { jobId, messageId: result.messageId }}// Zaplanowanie z opóźnieniemexport async function scheduleReminder(userId: string, delayMinutes: number) { await requireAdmin() if (!userId || !Number.isInteger(delayMinutes) || delayMinutes < 1) { throw new Error('Nieprawidłowy użytkownik lub opóźnienie') } await qstash.publishJSON({ url: `${appUrl}/api/reminders`, body: { userId, type: 'trial-ending' }, delay: `${delayMinutes}m`, })}
Zmienne QSTASH_TOKEN oraz APP_URL są wykorzystywane wyłącznie po stronie serwera, dlatego nie wymagają publicznego prefiksu. Warto też pamiętać, że Server Action nie stanowi pełnej granicy zaufania, ponieważ przed uruchomieniem kosztownego zadania należy każdorazowo zweryfikować sesję użytkownika, jego rolę oraz poprawność danych wejściowych. Ponadto parametr retries: 3 oznacza dokładnie trzy dodatkowe ponowienia po pierwszej próbie, co daje łącznie maksymalnie cztery próby dostarczenia zadania.
Odbiór zadania QStash w Route Handler
Code
// app/api/cron/weekly-report/route.tsimport { verifySignatureAppRouter } from '@upstash/qstash/nextjs'type WeeklyReportJob = { jobId: string; type: 'weekly' }function isWeeklyReportJob(value: unknown): value is WeeklyReportJob { return ( typeof value === 'object' && value !== null && 'jobId' in value && typeof value.jobId === 'string' && 'type' in value && value.type === 'weekly' )}async function handler(request: Request) { const body: unknown = await request.json() if (!isWeeklyReportJob(body)) { // Błąd trwały: nie zużywaj ponowień na wadliwy payload. return new Response('Invalid payload', { status: 489, headers: { 'Upstash-NonRetryable-Error': 'true' }, }) } const report = await generateWeeklyReport() await sendReportEmail(report, { idempotencyKey: `weekly-report/${body.jobId}`, }) return new Response('OK')}// QStash automatycznie weryfikuje podpis. Żadne inne żądanie nie przejdzie.export const POST = verifySignatureAppRouter(handler)
Wrapper wymaga trzech sekretów z konsoli Upstash: QSTASH_TOKEN do publikowania oraz QSTASH_CURRENT_SIGNING_KEY i QSTASH_NEXT_SIGNING_KEY do weryfikacji rotowanych podpisów. Weryfikacja podpisu potwierdza nadawcę, ale nie zapobiega ponownemu dostarczeniu poprawnie podpisanej wiadomości. jobId musi więc trafić do trwałego klucza idempotencji, unikalnego indeksu albo transakcji. Funkcja sendReportEmail z przykładu powinna przekazać ten klucz do dostawcy wiadomości.
QStash schedules mają maksymalną rozdzielczość jednej minuty; dostarczenie pojedynczej wiadomości możesz natomiast opóźnić o sekundy. Jawny scheduleId pozwala aktualizować istniejący harmonogram zamiast przypadkowo tworzyć kolejny przy każdym wdrożeniu. Po wyczerpaniu ponowień obsłuż failureCallback albo kontroluj kolejkę niedostarczonych zadań (Dead Letter Queue). Adres failureCallback jest również publiczny, dlatego zabezpiecz go przez verifySignatureAppRouter, tak jak adres odbierający zadanie.
GitHub Actions cron jako alternatywa
Dla prostych zadań GitHub Actions może uruchamiać workflow według harmonogramu:
Najkrótszy obsługiwany interwał wynosi 5 minut, ale GitHub nie daje nam jakichkolwiek gwarancji punktualnego startu. Przy dużym obciążeniu workflow może się opóźnić, a oczekujące uruchomienie nawet wypaść (szczególnie obciążony bywa początek godziny). Harmonogram działa tylko z domyślnej gałęzi, a w publicznym repozytorium zostaje automatycznie wyłączony po 60 dniach bez aktywności. Koszt zależy od widoczności repozytorium, użytego runnera i planu GitHub, więc nie zakładaj stałego limitu 2000 darmowych minut.
Czy cron powinien działać na Edge Runtime?
Edge Runtime nie jest harmonogramem, ale środowiskiem wykonania, więc nadal potrzebujesz Vercel Cron, QStash, GitHub Actions albo systemowego crona, który wywoła adres zadania. W Next.js domyślnym i rekomendowanym środowiskiem wykonania Route Handlera jest Node.js. Wybór export const runtime = 'edge' ma sens tylko wtedy, gdy wszystkie zależności zadania, w tym klient bazy i SDK dostawców, obsługują Edge. Nie omija też limitów czasu platformy.
after() z Next.js również nie zastępuje crona. Pozwala kontynuować pracę po wysłaniu odpowiedzi, ale callback nadal działa w ramach maxDuration tego samego wywołania i sam nie uruchomi się o wybranej godzinie. Podobnie setInterval lub timer utworzony przy starcie modułu jest niewiarygodny w środowisku bezserwerowym: instancja może zostać zamrożona albo usunięta w dowolnym momencie.
Idempotencja, blokady i monitoring zadań cron
Nie projektuj zadania na założeniu dokładnie jednego wykonania. Vercel może zarówno pominąć, jak i powtórzyć termin. QStash wykonuje skonfigurowaną liczbę ponowień, więc ta sama wiadomość może dotrzeć więcej niż raz. GitHub Actions może wystartować później lub pominąć oczekujące wykonanie przy dużym obciążeniu.
Idempotencję buduj na stabilnym kluczu biznesowym, np. daily-report/2026-07-16 albo ID konkretnego zdarzenia. Zapisuj go w bazie z unikalnym indeksem i używaj transakcji tam, gdzie skutek dotyczy danych. Dla zewnętrznych efektów ubocznych korzystaj z kluczy idempotencji dostawcy. Deduplication QStash ogranicza ponowne opublikowanie wiadomości w dziesięciominutowym oknie, ale nie zwalnia odbiorcy z obsługi powtórnego dostarczenia.
Jeżeli dwa wykonania nie mogą działać równolegle, zastosuj blokadę we współdzielonym magazynie z właścicielem i TTL dłuższym od typowego czasu zadania. Zwolnienie blokady umieść w finally, ale nie polegaj wyłącznie na ręcznym zwolnieniu, bo proces może zostać przerwany. Dla dużej liczby elementów zapisuj punkt kontrolny i przetwarzaj małe porcje, aby następne wykonanie mogło bezpiecznie wznowić pracę.
Monitoruj początek, koniec, czas trwania, liczbę przetworzonych rekordów, identyfikator uruchomienia i wynik. Zwracaj 2xx dopiero po rzeczywistym sukcesie; błąd przejściowy powinien dać 5xx, aby QStash wykonał ponowienie. Oprócz logów ustaw zewnętrzny sygnał kontrolny z maksymalnym dopuszczalnym opóźnieniem oraz alert na brak wykonania, bo brak żądania nie wygeneruje wyjątku w aplikacji.
Vercel Cron vs Upstash QStash vs GitHub Actions
Metoda
Minimalny harmonogram
Ponowienia
Sposób rozliczania
Najlepsze zastosowanie
Vercel Cron
Hobby: dobowy; Pro: minuta
Brak automatycznych
Zużycie Vercel Functions
Proste zadania cykliczne
Upstash QStash
Minuta; opóźnienie od sekund
Automatyczne, konfigurowalne
Wiadomości i wywołania
Ponowienia, kolejki i opóźnienia
GitHub Actions
5 minut
Logika workflow lub ręczny rerun
Minuty runnera według planu
Rzadkie zadania repozytorium
Cron hostowany samodzielnie
Zwykle 1 minuta
Własna implementacja
Serwer i utrzymanie
Pełna kontrola
Elastyczne i wydajne narzędzia dla biznesu, które dotrzymają kroku Twojemu rozwojowi.
Są dostępne na wszystkich planach, ale wywołania zużywają limity Vercel Functions. Według dokumentacji zweryfikowanej 17 lipca 2026 plan Hobby pozwala na 100 definicji na projekt, najwyżej jedno uruchomienie dziennie i precyzję godzinową (do ±59 minut). Pro i Enterprise pozwalają na 100 definicji, interwał minutowy i precyzję minutową. Cennik oraz limity mogą się zmienić, więc sprawdź je ponownie przed wdrożeniem.
Jak monitorować zadania cron?
Na Vercel przejrzysz wywołania w logach, filtrując po ścieżce zadania cron (np. /api/cron/). QStash udostępnia własny dashboard z historią dostarczeń, opóźnień i ponowień, co jest szczególnie cenne przy zadaniach z ponawianiem. Dodaj alerty na błędy oraz zewnętrzny sygnał kontrolny, bo Vercel uruchamia crony bez gwarancji każdego terminu. Pominięte wywołanie może nie utworzyć żadnego logu funkcji, więc samo Sentry nie wykryje jego braku.
Co, jeśli zadanie cron trwa dłużej niż limit czasu funkcji?
Funkcje bezserwerowe mają limit czasu wykonania, który zależy od planu, więc długie zadania trzeba rozbić. Podziel pracę na mniejsze, idempotentne kroki w QStash lub Upstash Workflow albo przenieś ją do trwałego procesu roboczego. Samo after() nie omija limitu czasu. Działa tylko do maxDuration danej funkcji. QStash również ma planowy limit czasu odpowiedzi pojedynczego adresu.
Dlaczego trzeba zabezpieczać adres crona tokenem?
Bo Route Handler crona to zwykły, publicznie dostępny adres URL. Bez weryfikacji każdy w internecie mógłby go wywołać i np. uruchomić masową wysyłkę maili albo czyszczenie danych poza harmonogramem. Vercel rozwiązuje to, dołączając CRON_SECRET w nagłówku autoryzacji, który sprawdzasz na wejściu; QStash używa podpisu kryptograficznego weryfikowanego przez verifySignatureAppRouter. W obu przypadkach żądanie bez poprawnego sekretu jest odrzucane. Sprawdź też, czy zmienna środowiskowa istnieje, bo porównanie z Bearer undefined nie jest zabezpieczeniem.
Vercel Cron czy QStash, co wybrać?
Vercel Cron jest najprostszy dla zadań czysto cyklicznych uruchamianych o stałych porach, takich jak raporty lub czyszczenie danych, i nie wymaga dodatkowej usługi, jeśli i tak hostujesz na Vercel. QStash wybierz, gdy potrzebujesz opóźnień („wyślij za 30 minut"), automatycznego ponawiania po błędzie albo kolejkowania zadań wyzwalanych zdarzeniem, a nie tylko zegarem. W wielu projektach oba współistnieją: Vercel Cron do harmonogramu, QStash do zadań opóźnionych i zawodnych.
Dlaczego zadanie cron musi być idempotentne?
Harmonogram może uruchomić tę samą pracę więcej niż raz, a kolejne wykonanie może zacząć się przed zakończeniem poprzedniego. Vercel dokumentuje zarówno możliwe duplikaty, jak i pominięte wywołania, a QStash gwarantuje dostarczenie co najmniej raz. Używaj trwałych kluczy idempotencji, unikalnych ograniczeń w bazie i blokad z terminem wygaśnięcia. Nie zakładaj dokładnie jednego wykonania.
O autorze
Maciej Sala
Maciej Sala — Product Manager i Frontend Developer z bogatym doświadczeniem w marketingu internetowym oraz SEO. Na co dzień pracuje z Reactem, Next.js i TypeScriptem, a ostatnio także z Astro i narzędziami do automatyzacji procesów AI. Sprawnie łączy perspektywę produktową z praktycznym podejściem do kodu. Przez kilka lat był związany z branżą gier wideo jako project manager i game designer. Absolwent historii na Uniwersytecie Jagiellońskim oraz studiów podyplomowych z marketingu internetowego na AGH w Krakowie. Po godzinach trenuje na siłowni, maluje figurki i rozwijam własne projekty.
To trzecia część mojej małej serii „Backend dla frontendowca” i po fundamentach API oraz tematach real-time, webhooków i uwierzytelniania zostaje warstwa, która decyduje o tym, czy aplikacja wytrzyma prawdziwe użytkowanie: cache , kolejki, pliki, deployment, monitoring i bezpieczeństwo .
Maciej Sala
Founder StriveLab
Jeden bot, jedna pętla z błędem albo jeden zdeterminowany atakujący — i Twoje API dostaje tysiące żądań na minutę. Serwer się dławi, rachunek za infrastrukturę rośnie, a normalni użytkownicy widzą błędy. Rate limiting ogranicza liczbę żądań z jednego źródła w oknie czasu. Pokazuję, jak wdrożyć go w Next.js z Upstash Redis — w Proxy, Route Handlerach i Server Actions, z trzema algorytmami i limitami per endpoint. W Next.js 16 proxy.ts działa jednak w Node.js, więc „na Edge” dotyczy Route Handlera z Edge Runtime albo infrastruktury dostawcy, nie samej konwencji Proxy.
Maciej Sala
Founder StriveLab
Vercel AI SDK zdejmuje z Twoich barków większość żmudnej roboty przy dodawaniu AI do aplikacji w Next.js. Zapomnij o ręcznym czytaniu i zarządzaniu strumieniem bajtów z API, parsowaniu chunków czy doklejaniu poprzednich wypowiedzi do historii rozmowy. W tym tutorialu zbudujesz działającego, streamującego chatbota w mniej więcej 30 minut — dwa pliki i kilka linii konfiguracji.