You deployed on Railway because you wanted to ship fast, not to learn Kubernetes cronjob syntax. Then your product manager asked for the small thing: "Send the morning push every day at 08:00 in the user's timezone, plus a weekly Sunday digest." You opened the Railway dashboard, looked for the Cron tab, and there is no Cron tab. Railway does not ship a scheduler. Here is what teams on Railway are actually doing instead.
If you just want the short version: keep your Railway service, expose the work behind a POST route on your existing *.up.railway.app domain, check a bearer token in the handler, and point Crontap at the URL. You get every 1 minute on Pro, per-IANA timezones, per-job failure alerts, and one dashboard across every Railway project (and every non-Railway target) you own.
Confirming Railway has no scheduled-HTTP primitive
It is worth being precise about this because the answer to "does Railway have cron" depends on what you mean. The Railway docs describe services, processes, and deployments. There is no first-class "schedule this HTTP endpoint at this cadence" primitive. There is no equivalent of Vercel Cron, no equivalent of Cloud Scheduler, no equivalent of Heroku Scheduler.
What Railway gives you is a long-running container. If you want a clock, you bring your own. That is fine for some shapes (a worker process running BullMQ, Sidekiq, or APScheduler is a normal Railway pattern), and it is overkill for others (one HTTPS call every five minutes does not need a dedicated container).
The three options teams try
Before reaching for an external scheduler, most teams try the obvious ones. The honest answer is that two of the three work but cost more than they look like they cost; the third is what most teams settle on.
Option 1: Always-on worker container running APScheduler or node-cron
You spin up a second Railway service. It runs forever. Inside it, a process imports apscheduler (Python) or node-cron (Node) and registers callbacks at the cadences you want. When a callback fires, it calls your main service's HTTP endpoint, or runs the work in-process.
This works. You will see this pattern in countless Railway tutorials. The catch is the bill and the operational shape:
- The container is up 24/7, billing memory and CPU even when nothing is firing. For a handful of triggers per day, that is a full service worth of resources to be a clock.
- The cron expressions live in code. To change a cadence you redeploy. The same redeploy story Vercel Cron has, except now you are also paying the always-on cost.
- A crash of the worker silently kills every schedule. You either add a healthcheck and an alert (which is itself another service), or you find out a week later that the Sunday push did not go.
- Multi-process gotchas. If you scale the worker beyond one replica, you fire each schedule N times unless you add a leader-election layer.
For "I have 30 background jobs and they share a queue", a worker is genuinely the right answer. For "I have 5 cadences and they are all HTTPS calls", a worker is a heavyweight tool.
Option 2: GitHub Actions cron (with the drift caveat)
GitHub Actions has a schedule event. You write a workflow that fires on a cron expression and runs a curl against your Railway URL. It is free for public repos, generous for private, and you do not run any infrastructure.
The wall is in the docs themselves. GitHub explicitly notes that the schedule event "may be delayed during periods of high loads of GitHub Actions workflow runs". In practice, your "every 5 minutes" job lands somewhere in a 5 to 15 minute window, and your 08:00 Sunday digest sometimes arrives at 08:11. For "send the morning push at exactly 08:00 in the user's timezone", that drift is not acceptable. For nightly housekeeping, it is fine.
Other GitHub Actions caveats worth naming:
- Cron expressions in
on.schedulerun on UTC. Per-IANA timezone is not a field. You compute it yourself. - The minimum cadence is every 5 minutes per the schedule docs, and even that is best-effort.
- Failure alerts route to GitHub notifications by default. If you want Slack or webhook alerts, you wire it in via another action.
If your team already lives in GitHub Actions and the drift is fine, this is the lightweight option. The honest framing is "this is duct tape until you hit the second cadence-sensitive use case".
Option 3: External HTTP cron
The third option is to put the clock outside Railway entirely. An external scheduler holds the cadences, the timezones, the failure alerts, and the dashboard. Railway holds the runtime. The contract between them is one HTTPS call per cadence with a bearer token.
Crontap (cron) → HTTPS POST → https://your-service-production.up.railway.app/api/jobs/your-job → the actual work
This is the shape we recommend for HTTPS-triggered work, and it is the shape this guide walks through.
Why external cron wins for pure HTTP triggers
A lot of teams reach option 3 after trying 1 or 2. The argument is roughly:
- It does not run a container. No always-on worker, no idle CPU, no "the clock is also a service I have to deploy". Railway keeps doing what it is good at (running the app); the scheduler does what it is good at (firing on time).
- Cadence changes are not redeploys. "Every 5 minutes" to "every 15 minutes" is a dropdown in the dashboard. No PR, no review, no waiting for the build.
- Per-schedule timezone is a field. A morning push at 08:00 America/New_York and a Sunday digest at 08:00 Europe/Berlin are two schedules with their own zone field. Crontap handles DST transitions automatically.
- Failure alerts are first-class. When the Railway endpoint returns a 502 because the deploy is mid-rollout, the alert lands in the channel of your choice with the response body and timing inline. No "go check the worker logs to see if it ran".
- One dashboard across projects and providers. A team running on Railway plus AWS Lambda plus a customer's Vercel app gets one cron tab for everything, not three.
The shape works whether your code is on Railway, on Render, on Fly.io, on a customer's box, or split across all of them. The schedule lives outside the deployment.
The setup
Here is the minimal walkthrough. Use it as a starting point; the same pattern fits more complex jobs.
Step 1: Deploy your service normally
You probably already have a Railway service deployed at something like https://your-service-production.up.railway.app. If not, deploy whatever your app needs (Node, Python, Go, Rust, Bun, Elixir, anything that speaks HTTP). Railway gives every service a public URL by default; that is the URL Crontap will hit.
Step 2: Add an HTTP endpoint for the scheduled work
Create a POST route inside your existing app for each scheduled job. The route is the boundary; everything inside it is your normal code.
export async function POST(request: Request) {
const auth = request.headers.get("authorization");
if (auth !== `Bearer ${process.env.CRON_SECRET}`) {
return new Response("Unauthorized", { status: 401 });
}
await sendMorningPush();
return Response.json({ ok: true });
}
Three things to call out:
- The route is
POSTso a casualGETfrom a crawler does not trigger it. - The token rides in the
Authorizationheader, not a query string. The secret never lands in access logs. - The handler returns 200 quickly. If the actual work is slow (a fan-out push to thousands of users), kick it off as a background task, write to your queue, and return; do not block the scheduler waiting for a 30-second job to finish.
Generate a strong random secret locally and store it in Railway as a service variable named CRON_SECRET:
openssl rand -base64 32
In the Railway dashboard, open your service, go to Variables, click New Variable, set CRON_SECRET to the value, and redeploy once so the variable is live.
Step 3: Crontap schedule pointing at the Railway URL
Head to Crontap and create a new schedule.
- URL. Paste the production URL of your endpoint, e.g.
https://your-service-production.up.railway.app/api/jobs/morning-push. - 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 5 minutes") or paste a cron expression. Crontap previews the next 5 fires inline so you can sanity-check before saving.
- Timezone. Pick the IANA zone that matches the schedule's intent. America/New_York for the 08:00 ET morning push, Europe/Berlin for the 08:00 CET digest, UTC for anything that is genuinely UTC.
- 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.
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 →
Real customer examples (anonymized)
The pattern shows up in real shapes. One we have watched closely is a Railway-hosted API firing push notifications weekly, monthly and quarterly. The cadence mix tells the story:
- Every 2 minutes for a release-redemption sweep (the API checks for in-flight redemptions and finalises them before they expire).
- Every 5 minutes for a jobs index endpoint that the mobile app polls indirectly via an internal CDN.
- Every 30 minutes for a morning and evening push ingestion (each push gathers candidates, hands them to the notification service, and returns 200 quickly).
- Weekly Sunday at 08:00 America/New_York for the weekly engagement digest.
- Monthly on the 1st at 08:00 America/New_York for the monthly summary.
- Quarterly for the quarterly recap.
That is six schedules with three different cadences and one fixed timezone, against three Railway services in one Railway project. With an always-on worker, that is one extra Railway service to operate (and pay for) for what is, in practice, a small bag of curl calls. With external cron, it is six entries in Crontap's dashboard, no extra Railway service, and the on-call pager routes to one Slack channel.
If your shape looks similar (a few cadences, mixed weekly/monthly/quarterly, one or two timezones), the external cron pattern is almost certainly the cleaner answer. If your shape is "thirty background jobs sharing a queue", the worker is still the right answer; do not let an external scheduler fight your job system.
FAQ
Does Railway not have any cron at all?
Correct, as of the Railway docs at the time of writing. Railway runs services and processes; the scheduling primitive is "your container is up and you ran a scheduler library inside it". There is no first-class HTTP scheduler tab in the dashboard.
Can I just run node-cron inside my main service?
You can. The catch is that your main service is now coupled to scheduling: a deploy bounces the schedules, a crash drops them, and scaling beyond one replica fires every job N times unless you add leader election. For a handful of HTTPS triggers, an external scheduler avoids all three problems. For ten internal background jobs, the in-process scheduler is the right call.
What about railway run cron?
railway run is a CLI command, not a scheduler. It runs a command once with your project's environment loaded. You could combine it with your laptop's crontab, but then your laptop is the cron server. Not recommended for anything production.
Can Crontap call private Railway services?
Crontap is HTTPS-only and hits public URLs. Railway services have a public URL by default (*.up.railway.app), which is what Crontap targets. If you have configured a custom internal-only domain, expose a thin public proxy that authenticates the bearer and forwards into the internal address. The bearer-in-handler pattern is the audit boundary.
Does this work with PR previews?
Yes. Each Railway PR preview gets its own URL. You can either schedule the preview URL temporarily in Crontap (and clean it up when the PR merges), or skip scheduling on previews and run the cadence only against production. Most teams do the latter.
What if my route takes 30 seconds to finish?
Do not block the scheduler waiting for the work. Have the route enqueue or kick off a background task, write to your audit log, and return 200 immediately. Crontap will respect a long-running response, but you do not want the scheduler to be your queue. If you do not have a queue yet, the simplest pattern is setImmediate(work) (Node) or a background thread (Python) inside the handler, then return.
References
Related on Crontap
- Cron jobs for Railway. The use-case-first guide for Railway users wiring up an external scheduler.
- Heroku Scheduler alternative. The same pattern for Heroku apps.
- Vercel Cron every minute: the Hobby hourly limit and how to beat it. The Vercel-flavored version of the same external-cron arc.
