You set up a Render Cron Job for the staging service, copy-pasted the schedule into the prod service, and accepted that the cadence now lives in two places. A month later you renamed the endpoint, redeployed staging, and only realised the prod cron was still firing the old path when the Slack alert finally caught up. Render Cron Jobs are clean if you have one service in one environment. Past that line, the per-service shape costs you more than it saves. This post walks through what Render Cron does well, where the friction shows up, and the external cron pattern most multi-service Render teams end up running instead.
If you want the short version: keep your Render service, expose the work behind one HTTPS route on the *.onrender.com URL (or your custom domain), check a bearer token in the handler, and point Crontap at it. You get a single dashboard for every cron across every service and environment, per-IANA timezones, decoupled-from-redeploys behaviour, and a free tier where Render Cron starts on a paid plan.
What Render Cron Jobs do today
Render Cron Jobs are a first-class service type in Render's platform. You create a service, point it at a Docker image or a buildpack, give it a 5-field cron expression, and Render runs the container on cadence. Each fire is a fresh container that boots, runs your command, and exits. Logs land in the service's log stream.
The shape is intentionally simple:
- Cadence. Standard 5-field cron expressions. The cadence floor is 1 minute (the same floor Crontap Pro hits, so cadence itself is not the wedge here).
- Plan. Render Cron Jobs are paid-tier only, starting on the Individual plan. There is no free tier for cron.
- Execution model. Each cron fire spins a container with your code baked in. The container exits when the run completes. You pick the instance type per cron service, the way you pick it for any other Render service.
- Scoping. A Render Cron Job is its own service. Its config (cadence, command, env vars, secrets) lives on that service. There is no shared scheduler across services in your account.
- Logs. The container's stdout and stderr land in the Render dashboard for that cron service. You scroll the same way you would for a web service.
That's the whole product surface. If your app has one service, one environment, and one or two crons that share the same image, Render Cron is a clean fit. The friction shows up when any of those three "ones" multiply.
Where Render Cron starts to bite
The pattern that costs you in production is the per-service scoping. It looks like a small detail in the dashboard, and turns into three concrete walls once you have more than one service or more than one environment.
Per-environment duplication
Most teams running Render in production have at least two environments. Prod is the customer-facing service. Staging (or a long-lived QA branch) is the second copy of the same app. Both run cron-style work: maybe a /crons/minute health pulse, maybe a /crons/day reconciliation, maybe both.
On Render, those two environments are two separate services. Each service carries its own cron config. If you want the same every minute job firing on both, you set it up twice. If you want to change the cadence, you change it twice. If you rename the endpoint, you rename it twice. The configs sit in two places and quietly drift apart.
The drift is rarely loud. It's a Slack alert that fires from staging at 03:14 because someone tweaked the cron expression in staging and not prod. It's a discovery six weeks later that the prod schedule has been firing the wrong cadence since the last service rename. The cost is in the trail of small inconsistencies that nobody sees until something breaks.
Redeploy drift
Render Cron Jobs run inside a service container. The image, the env vars, the build, and the cron schedule are coupled to the service's deploy lifecycle. A redeploy of the cron service rebuilds the container and applies whatever cron config is in the dashboard at that moment.
Two consequences:
- Cron config and code config travel together. If you want to keep the schedule but change the image, you redeploy. If you want to change the schedule without touching the image, you still touch the service. Most teams reach for IaC (Render Blueprints, Terraform) the moment they have more than one cron, which is fine, except now schedule changes go through a PR review and a CI build to land.
- Mid-deploy fires are not free. If your cron service is mid-rebuild when the cadence wants to fire, the run is delayed or skipped depending on the state. You find out after the fact that the daily reconciliation did not run because the deploy was queued behind two other Render builds.
For a single cron in a single service, redeploy coupling is a feature, not a bug; you genuinely want the schedule to ship and roll back with the code. For a fleet of crons across multiple services, the coupling is a tax you pay every time you want to tune one cadence.
Cross-environment parity
The third wall surfaces in code reviews. You want prod and staging to fire the exact same cron at the same cadence with the same headers. The only difference is the URL prefix. The native shape forces you into either two duplicated cron services that drift the way duplicated YAML always drifts, or one cron service with a runtime if (env === "prod") guard that exists only to dodge a config gap. Neither path is broken; both quietly add operational surface area. The pattern that ages well is to keep the cron logic in the service code (one route, one handler, one set of tests) and let the schedule itself decide what runs where.
The external cron pattern
The shape works because Render services already expose a public HTTPS URL. Crontap fires HTTP at the URL on cadence. Render still owns the runtime. Crontap owns the clock. The contract between them is one HTTPS POST per cadence with a bearer token.
Crontap (cron) → HTTPS POST → https://your-service.onrender.com/crons/<job> → the actual work
That's it. Two pieces. The cron config lives outside the service container, so redeploys do not change it. The schedule lives in one dashboard, so prod and staging are two entries in the same table, not two duplicated YAML blocks in two different services.
Step 1: Add the route to your existing service
Most Render services already have a web framework wired up. The translation is a thin HTTP handler that calls into the same code path the old cron service was running.
import { type Request, type Response } from "express";
export async function refreshTokensCron(req: Request, res: Response) {
if (req.headers.authorization !== `Bearer ${process.env.CRON_SECRET}`) {
return res.status(401).json({ error: "Unauthorized" });
}
await refreshTokens();
return res.status(200).json({ ok: true });
}
Three things to note:
- The route reads the
Authorizationheader and refuses anonymous requests. Without this, your cron URL is a public POST endpoint anyone can fire. - The work returns 200 quickly. If the actual work takes longer than the HTTP request you want to keep open, push the work onto a background job (BullMQ, Celery, ActiveJob, whatever your stack already uses) and return 200 from the route. Crontap is a scheduler, not a queue.
- The endpoint is the same shape for prod and staging because each environment gets its own
*.onrender.comURL automatically. We will use that.
Step 2: Add the secret as a Render env var
Generate a long random string locally:
openssl rand -base64 32
Add it as CRON_SECRET under Environment in the Render dashboard for the service. Apply to prod. Apply (the same value or a different one) to staging. Render reads env vars per service, so the two services can carry different secrets if you want full isolation. Redeploy once so the variable is live.
If you already use a secrets manager (Doppler, 1Password CLI, Render Environment Groups), wire it up the same way you do any other secret. The point is that the route refuses anonymous traffic.
Step 3: Point Crontap at the service URL
Head to Crontap and create a new schedule.
- URL.
https://your-service.onrender.com/crons/refresh-tokens. Custom domain works the same. - Method.
POST. - Headers. Add
Authorization: Bearer <your CRON_SECRET>. Crontap stores the value encrypted; you do not see it again after saving. - Cadence. Type plain English ("every minute", "every day at 04:00") or paste a cron expression. Crontap previews the next 5 fires inline so you can sanity-check before saving.
- Timezone. Pick the IANA zone the schedule actually runs in. Render Cron is UTC; Crontap is per-schedule, so a daily 04:00 in
Europe/Madridand another at 04:00 inAmerica/Santo_Domingoare two separate timezone fields, no UTC math. - Failure alerts. Add an integration: email / webhook (Slack / Discord / Telegram). Crontap fires on 4xx and 5xx with the response body and timing in the payload, so a Slack alert lands the moment the route returns 401 or 500.
Press Perform test to fire a real request before you trust the cadence. If the route returns 200, you are done. If you see 401, your bearer is mismatched. If you see 5xx, the route itself failed and the alerting just proved itself.
Fix this in 60 seconds with Crontap. Free tier available. No credit card. Schedule your first job →
Worked example: prod and staging in two environments
Here is a pattern straight out of the Render comparison page. Take a Render team running two environments of the same app: prod in Europe/Madrid and staging in America/Santo_Domingo. Both environments have two cron-style endpoints: /crons/minute for a per-minute health check and /crons/day for a daily 04:00 reconciliation.
Old shape (Render Cron Jobs): four cron services. Two for prod (prod-minute, prod-day), two for staging (staging-minute, staging-day). Each service has its own cron config in the dashboard. If you want to change the daily run from 04:00 to 04:15, you do it in two places. If you want to add a new cron, you do it twice. The schedules drift the moment two engineers make changes in two different weeks.
New shape (external cron): four Crontap schedules in one dashboard. Same cron expressions per pair, different URLs, different timezones if you want them.
| Schedule | URL | Cadence | Timezone |
|---|---|---|---|
| prod-minute | https://prod.onrender.com/crons/minute | * * * * * | Europe/Madrid |
| prod-day | https://prod.onrender.com/crons/day | 0 4 * * * | Europe/Madrid |
| staging-minute | https://staging.onrender.com/crons/minute | * * * * * | America/Santo_Domingo |
| staging-day | https://staging.onrender.com/crons/day | 0 4 * * * | America/Santo_Domingo |
To change the daily cadence, you edit two rows in one table instead of touching two services. To add a new cron, you copy a schedule and change the URL. To pause staging while you ship a noisy refactor, you toggle two rows off. Same Authorization: Bearer ... header on each, different bearer secret per environment if you want full isolation. The cron service type is gone, the duplication is gone, and prod and staging are visibly side by side every time you open the dashboard.
If the staging cadence later needs to drop to every 15 minutes (because every minute is too noisy on a low-traffic environment) that's a dropdown change in two rows. No deploy. No service config edit.
When to keep using Render Cron Jobs
External cron is a shape, not a religion. Cases where the built-in is the right answer:
- One service, one environment, one or two crons. The duplication tax does not exist if there is nothing to duplicate.
- The cron job genuinely needs same-process logging next to the service runtime, and you want the cron to ship and roll back with the code.
- The work is heavy enough that you want a dedicated container per fire (a long Spark job, an image-rendering pass, anything that benefits from isolation).
- You are already running everything else on Render and one fewer external dependency is worth more than the multi-environment polish.
For everything else, the external pattern reads cleaner. Many teams pair the two: keep one heavy in-service Render Cron Job where same-process logging matters, and add Crontap for the rest so the multi-environment crons live in one dashboard. The two do not conflict.
FAQ
Can Crontap call my Render service on the free plan?
Render's free instance type for web services has cold-start behaviour and goes to sleep after inactivity. A Crontap call wakes the instance the same way any other HTTPS request would. If your cadence is shorter than the sleep window, the service effectively never sleeps. Render Cron Jobs themselves require a paid plan; Crontap Pro is $3.25/mo billed annually for unlimited schedules at every-minute cadence, and there is a free tier as well.
Will Crontap and Render Cron conflict if I run both?
No. They run independently. Many teams keep one Render Cron Job for an in-service heavy task (where same-process logging matters) and add Crontap for everything that needs cross-environment parity, per-IANA timezones, or a single dashboard.
Does Crontap work with Render preview environments?
Yes. Point a separate Crontap schedule at each environment's URL: prod at prod-app.onrender.com, staging at staging-app.onrender.com, custom domains the same way. Per-schedule timezones, headers, and payloads stay independent so prod and staging can carry different config without duplicating cron services.
Will this save me money?
Often. Render Cron Jobs are paid-tier only and each cron is its own service, so a fleet of small crons stacks instance costs. Crontap calls a route on the web service you already pay for; the marginal cost per fire is one HTTPS request. If your Render bill has more than two cron services on it, the external pattern usually pays for itself in the first month.
Does this require a public HTTP endpoint?
Yes, on the same *.onrender.com URL your service already exposes. The endpoint is auth-protected: Crontap sends an Authorization header you control, and your route checks it before doing any work. If "no public endpoint at all" is a hard requirement, the right answer is to stay on Render Cron Jobs with the per-service cadence.
References
Related on Crontap
- Crontap vs Render Cron Jobs, side by side. The head-to-head decision page with the at-a-glance table.
- Cron jobs for Render spoke. The Render-specific guide to running scheduled work without per-service cron config.
- Heroku Scheduler alternative: full cron expressions without the add-on. The PaaS sibling post; the same external-cron shape applied to Heroku.
