Where should files live in a Vercel app, and how much work should a cron do?

  • Isaac
    Isaac
    AI Systems Engineer

Files belong in Vercel Blob, S3-backed object storage with private or public stores, and scheduled work belongs in Vercel Cron Jobs, which are plain HTTP GET calls to your production deployment on a UTC schedule. In Monogram Tools we keep every generated file in a private Blob store and serve it through our own routes or signed URLs, and our 15 crons follow two patterns, with 13 doing their work inside their own invocation and 2 reconciling and handing work to durable Vercel Workflows runs.

Should a Vercel Blob store be private or public?

Choose before you create it, because the access mode cannot change after the store exists, and neither can its region. A public store hands out URLs anyone can open, delivered straight from the CDN. A private store requires authentication for every read, and your own code decides who sees what.

Monogram Tools keeps everything it produces as a file in one private store, from published decks to pictures and video. Every public read starts in a route we wrote, which either streams the file or hands out a signed URL for it.

How do you serve files from a private Blob store?

The standard pattern is a route that authenticates the request, reads the blob with get() from @vercel/blob, and streams it back, as Vercel's private storage guide shows. Your route then owns the Cache-Control header.

Published decks work this way. A route resolves the deck, refuses anything unpublished, and streams the static Slidev site from private Blob. The deck's built assets are cached as immutable for a year, while its index.html revalidates on every request, so a republished deck shows up at once without throwing away the cache for files that never change. Pictures follow the same shape: they are written to the store as private and read back through an app route.

How do you upload or serve files larger than a function allows?

Keep the bytes out of your functions. A Vercel Function's request and response bodies are capped at 4.5 MB, and Vercel itself advises against serving private files over 100 MB through functions unless traffic is low. For uploads, the browser sends the file straight to Blob with a short-lived, path-scoped credential your server issues, a client token or a presigned PUT URL. For downloads, presigned GET URLs let the CDN serve a private blob directly for a limited time.

Screen Recorder is built on both. Our code states the rule plainly: nothing large passes through a function. The recorder and the Vercel Sandbox jobs upload with credentials our server issues, each scoped to one pathname. A recording's parts upload as it goes, and a server route can complete a multipart upload the browser started and could not finish. Every video read is a presigned GET URL, so the CDN answers the video player's range requests instead of our functions.

Why does an overwritten blob still show the old file?

Because Blob is cached, and an overwrite or delete can take up to 60 seconds to propagate. Vercel recommends treating blobs as immutable and writing new pathnames; for private stores, a read with useCache: false skips the cache when you must see the latest write.

This matters to us because Blob is also our signal store. When a Screen Recorder sandbox job finishes a stage, it writes a small marker file to Blob, and the durable run waiting on that job reads the marker, not the callback, to decide whether the stage landed.

Is Vercel Blob a replacement for S3?

For files an app on Vercel produces and serves, yes in practice; Vercel Blob runs on Amazon S3 underneath and inherits its durability. It is a narrower tool with its own SDK and Vercel's billing units, not a full S3 interface. If you need S3's own features, or your data is shared with systems outside Vercel, Vercel also lists Amazon S3 in its Marketplace.

Why isn't my Vercel Cron Job running?

Most reports come down to causes Vercel documents. Crons only invoke the production deployment, never previews. Schedules are always UTC. Hobby plans run a cron at most once a day, anywhere within the scheduled hour, and a more frequent expression fails the deploy. Crons do not follow redirects, so a route that answers with a 3xx does nothing. And there is no local cron runner; you call the route yourself.

Monogram Tools runs 15 schedules, from a monthly analytics rollup and a weekly Monday 06:00 UTC snapshot bake to several that tick every minute. Crons never run on our staging environment, a Vercel custom environment, so staging never sends a lifecycle email.

Does Vercel retry a failed cron, and can one run twice?

No, and yes. Vercel does not retry a failed invocation, delivery is best effort so a run can be missed, and the same scheduled run can occasionally arrive twice. A long job can also overlap the next tick. Vercel's advice is idempotent, reconciliation-based jobs plus a lock against overlap.

Our crons take that advice in two ways. Senders are idempotent: our lifecycle mail drain does not repeat a send when two runs overlap or the same scheduled run arrives twice. Workers that claim items use database leases; the Campaigns worker and the Workflow Builder's resume cron both lease what is due, so overlapping ticks take different items and a dead invocation only delays its items until the lease expires. Every cron route also refuses to run unless the request carries the project's cron secret, which Vercel sends with every scheduled call.

How much work should a cron do itself?

As much as fits in one invocation, and no more. A cron runs inside one Vercel Function invocation, so it has the same maximum duration. The question is whether the work can be cut into pieces that each tick finishes on its own, or whether a single unit of work needs retries, waits or more time than one invocation gives.

Bounded work in place is the right pattern for the first case, and 13 of our 15 crons use it. Mail drains, webhook deliveries, link checks, the monthly analytics rollup, clean-up sweeps and the weekly Sandbox snapshot bake all work inside the cron's own invocation, and the ones that could run long carry a budget or a fixed batch. Our lifecycle mail drain and short-link webhook drain stop claiming and sending a minute before their maximum duration and hand back what they did not reach for the next tick; the hourly link health check takes a fixed batch per run; the daily file sweep reads a bounded number of pages and reports whether more is left for tomorrow. The snapshot bake gives each snapshot 30 days of expiry against a 7-day schedule, so one missed run never leaves Deck Studio's PDF export without one.

Reconcile and hand off is the right pattern for the second case, and 2 of our crons use it. The Knowledge Base sweep, which runs every 5 minutes, is a reconciliation pass, not the retry itself. It finds documents still waiting on a retry and starts a durable Vercel Workflows run for each, and a duplicate run for the same document ends at once because the first one holds its hook. It only starts runs, and each run sleeps out its own backoff. Screen Recorder's safety-net cron does the same for clips whose durable run is missing or no longer live, and it also finishes a few small clean-up jobs in place.

When not to use Vercel Blob or Cron Jobs

Blob is not a database. It lists by prefix in lexicographic order and has no queries, so anything you need to search or join belongs in Postgres, with Blob holding the files the rows point at.

Cron is not a queue or a workflow engine. With no retries and best-effort delivery, a cron that must not lose work needs state behind it, either leases and outbox rows in the database or a durable Vercel Workflows run. If the schedule itself must be precise to the second, or must run more often than once a minute, look elsewhere.

For more on how we build on the platform, see our Vercel technology page and Next.js and Vercel development work, or contact us.

FAQ

How large can a file in Vercel Blob be? Up to 5 TB, with multipart uploads recommended above 100 MB.

How many cron jobs can a project have? 100 per project on every plan.

Can two crons share one route? Yes. Each request carries an x-vercel-cron-schedule header naming the expression that triggered it.