Home/Vercel cron timeouts

Run the job for hours. Return in under a second.

Vercel Hobby functions stop at 10 seconds. Pro stops at 60. The cron route only has to start the pipeline. GraphIngest runs the rest on managed infrastructure, for up to two hours on this template, and the route is already done.

  1. 01

    Cron hits your route

    Vercel calls /api/cron/sync on the schedule in vercel.json. That function is still subject to the platform timeout.

  2. 02

    The route returns

    triggerFlowRun hands the work off and responds with the run id. The HTTP call finishes in under a second.

  3. 03

    The pipeline keeps going

    sync-source runs for each source, in parallel, with retries. A two-hour timeout is a normal graph setting here.

Paste this route

Save it as app/api/cron/sync/route.ts. The flow id comes from the dashboard after you deploy the pipeline below. The API expects that id.

import { GraphIngestClient } from "graphingest";

export async function GET(request: Request) {
  const secret = process.env.CRON_SECRET;
  if (secret && request.headers.get("authorization") !== `Bearer ${secret}`) {
    return new Response("Unauthorized", { status: 401 });
  }

  const flowId = process.env.GRAPHINGEST_FLOW_ID;
  if (!flowId) {
    return Response.json({ error: "GRAPHINGEST_FLOW_ID is not set" }, { status: 500 });
  }

  const client = new GraphIngestClient();
  const res = await client.triggerFlowRun(flowId, {
    sources: [
      { name: "users", apiUrl: "https://jsonplaceholder.typicode.com/users" },
      { name: "posts", apiUrl: "https://jsonplaceholder.typicode.com/posts" },
    ],
  });

  const runId = (res as { data?: { id?: string } }).data?.id ?? null;
  return Response.json({ runId, status: "dispatched" });
}

Set GRAPHINGEST_API_KEY and GRAPHINGEST_FLOW_ID in the Vercel project. If you set CRON_SECRET, Vercel sends it as a bearer token and the route checks it.

The pipeline it starts

This is the part that used to live inside the cron handler. timeout_seconds=7200 is two hours. .map() syncs every source at once, and each source retries on its own.

from graphingest import node, graph, deploy, RetryPolicy
import requests

@node(name="sync-source", cache_ttl=1800, max_retries=3)
def sync_source(source: dict) -> dict:
    resp = requests.get(source["api_url"], timeout=30)
    data = resp.json()
    return {
        "source": source["name"],
        "records_synced": len(data) if isinstance(data, list) else 1,
    }

@graph(
    name="nightly-sync",
    timeout_seconds=7200,  # 2 hours
    retry_policy=RetryPolicy(max_retries=2, delay_seconds=30, backoff_factor=2),
)
def nightly_sync(sources: list[dict]):
    results = sync_source.map(sources)
    total = sum(r["records_synced"] for r in results)
    return {"sources_synced": len(results), "total_records": total}

if __name__ == "__main__":
    deploy()

Run it

  1. pip install graphingest requests, then run sdk/python/examples/vercel_cron_timeout.py. The TypeScript twin is sdk/typescript/examples/vercel_cron_timeout.ts.
  2. Copy the flow id from the dashboard into GRAPHINGEST_FLOW_ID.
  3. Put sdk/typescript/examples/vercel_cron_route.ts on the cron path and deploy the Next.js app.
  4. Trigger the cron. The route responds immediately, and the run shows up on the dashboard while the sync continues.