Railway Cron lifecycle: starts, retries, overlap, stopping, and domain variables
Anonymous
HOBBYOP

a month ago

Hello Railway team,

We are preparing a dedicated hourly Cron service for a short-lived Django

cleanup management command, separate from our Web service, on a Hobby

account. It would use a Dockerfile image, one replica, private database

networking, no public endpoint or volume, an explicit cleanup start command,

and an explicitly empty pre-deploy command. Source changes/autodeploys and

manual Run Now actions would be disabled during the validation.

We have read the Cron Jobs, Restart Policy and Variables documentation.

We understand that schedules use UTC, execution times can vary, and the

published Cron documentation says jobs must terminate themselves.

Before provisioning, could you clarify the following supported behavior?

  1. Start triggers and run identity

If the Cron schedule is configured before the first deployment, can the

initial deployment, schedule activation, or a later source/configuration

change start the command immediately outside the schedule? What supported

run/deployment identifier and start/end/exit metadata distinguish actual

executions from duplicated log lines?

  1. Automatic retries

Does restartPolicyType=NEVER apply to Cron executions? If not, what is the

actual retry behavior after failure, and is there a supported way to disable

automatic retries? Please distinguish retries from a later scheduled run.

Our proposed validation budget is two scheduled runs, not a claim that cron

provides exactly-once execution in all circumstances.

  1. Overlap

The docs describe skipping a tick while a prior run is still active. In the

Central Station thread "Cron job - run overlaps but next is not skipped"

(identifier 757fa4fd), an EMPLOYEE reply acknowledged contrary behavior at

the time. Has that issue been resolved for the current Cron implementation?

If provider-side exclusion is not supported, please say whether the job

must implement its own concurrency protection.

  1. Deadline

The Cron docs say Railway does not automatically terminate a stuck job.

Is there now a supported per-execution hard deadline? If not, please confirm

that bounded termination must be implemented by the application/entrypoint,

rather than assuming Web healthcheck or draining time is a Cron deadline.

  1. Disable and stop

What exact supported action prevents future and already queued executions

without converting the Cron service into an always-running service? What

separate action stops an in-flight run? If deletion of the dedicated service

is the supported method, what happens to queued/running executions and what

metadata confirms completion? We will not assume clearing cronSchedule is

an atomic pause operation.

  1. Domain-variable behavior without a public endpoint

Our shared staging validation checks the Web public-domain value even for

management commands. Can a Cron service with no generated/custom domain

explicitly set RAILWAY_PUBLIC_DOMAIN=${{web.RAILWAY_PUBLIC_DOMAIN}}? Does the

reference take precedence over the platform-provided variable in this case?

If that variable cannot be overridden, please identify the supported

alternative. We do not want to expose an HTTP endpoint solely for a cleanup

job or bypass our staging validation.

Explicit statements of what is not supported are useful. We are asking

about platform behavior, not permission to bypass application safeguards.

Please do not create, restart, deploy or modify any service while answering.

Thank you.

$10 Bounty

7 Replies

Railway
BOT

a month ago

This thread has been opened as a bounty so the community can help solve it.

Status changed to Open Railway • about 1 month ago


huyjgbnsss
FREE

a month ago

dsafdeq


huyjgbnsss
FREE

a month ago

dveqreqreqww


jpcrook11
PRO

a month ago

Railway cron is a scheduler bolted onto a normal service deployment — it starts your service's start command on a schedule. It is not a job runner, so there are no execution-level primitives: no run ID, no per-run retry policy, no deadline, no pause. Most of what you're asking for has to live in your code.

1. Start triggers and run identity. Yes — any deployment starts the command immediately, independent of the schedule. That includes the initial deploy when you provision, so expect one unscheduled run at creation even with autodeploys and Run Now disabled. Saving a schedule by itself doesn't trigger a run; deploying does. The injected metadata is RAILWAY_DEPLOYMENT_ID, RAILWAY_REPLICA_ID, RAILWAY_SNAPSHOT_ID — but the deployment ID is stable across executions of the same deployment, so it won't separate runs. Generate a UUID4 in your entrypoint and log it with start/end timestamps and exit code. That's your only reliable de-dup key.

2. Retries. The restart policy does apply; a cron execution is just that deployment's process. The default is On Failure with a maximum of 10 restarts, and Railway's own tooling notes that a cron service carries that default, so a failed job's exit code would turn one bad run into ten immediate reruns. NEVER is the supported way to disable this and is available on paid plans; only Free/trial accounts are restricted. Retries land seconds apart under the same deployment; a scheduled run lands at the next tick.

3. Overlap. Documented behavior is that a new execution is skipped while a prior one is still running, but that's a status check, not a lock — it depends on the previous deployment correctly reporting Active, and a container replaced mid-run can leave a row stuck, after which every later execution is skipped as an overlap. Manual redeploy and Run Now bypass the scheduler entirely. Treat provider-side exclusion as unsupported: take a pg_try_advisory_lock keyed on the command name and exit 0 if you don't acquire it.

4. Deadline. None. Railway does not automatically terminate deployments. RAILWAY_HEALTHCHECK_TIMEOUT_SEC is healthcheck length and RAILWAY_DEPLOYMENT_DRAINING_SECONDS is the SIGTERM-to-SIGKILL buffer — neither bounds a cron execution. Enforce it in the entrypoint: timeout --signal=TERM --kill-after=30 900 python manage.py cleanup.

5. Disable and stop. Clearing the cron schedule stops future triggers but is not atomic and won't touch an in-flight run. To stop a running execution, remove that deployment. Clearing the schedule leaves an ordinary service behind, but with NEVER and a command that exits it won't become always-on — it deploys once, exits, and stays stopped. Safe sequence: clear schedule, then remove the deployment. Deleting the service stops everything but destroys execution history with it, so it's teardown, not pause.

6. Domain variable. RAILWAY_PUBLIC_DOMAIN is the public service or customer domain and is only injected when the service actually has one, so on a domain-less cron service there's nothing to collide with — your explicit ${{web.RAILWAY_PUBLIC_DOMAIN}} is what the process sees. Cross-service references to that variable are a documented pattern. Precedence when both a platform value and a user value exist is not documented, so don't build on it: set your own key (PUBLIC_BASE_URL) and have Django settings read it with a fallback. Note the reference resolves empty if web has no domain in that environment.


shuaib-mk
FREE

a month ago

Cron on Railway is a scheduler on top of a normal service deployment, not a job runner, it starts your service's start command on a schedule. There's no run ID, no per-run retry policy, no deadline, and no pause built in. Most of what you're asking for has to live in your own code.

  1. Start triggers and run identity

Any deployment, including the initial one at provisioning, runs the start command immediately, independent of the schedule, so expect one unscheduled execution at creation even with autodeploy and Run Now disabled. RAILWAY_DEPLOYMENT_ID is stable across the same deployment, so it won't reliably separate individual scheduled ticks. Generate a UUID4 in your entrypoint and log it with start/end timestamps and exit code, that's your reliable de-dup key.

  1. Retries

The restart policy applies, a cron execution is just that deployment's process. Default is On Failure with a max of 10 restarts, which is documented to be able to turn one bad run into ten immediate reruns. Set restartPolicyType=NEVER to disable this. Retries land seconds apart under the same deployment; a genuine scheduled run lands at the next tick, that's how you tell them apart.

  1. Overlap

The documented behavior is that a new execution is skipped while a prior one is still marked active, but that's a status check, not a lock. A container replaced mid-run can leave that status stuck, and manual redeploys/Run Now bypass the scheduler entirely. Treat provider-side exclusion as unsupported: take a pg_try_advisory_lock keyed on the command name and exit 0 if you don't acquire it.

  1. Deadline

None. Railway does not automatically terminate deployments. RAILWAY_HEALTHCHECK_TIMEOUT_SEC and RAILWAY_DEPLOYMENT_DRAINING_SECONDS are unrelated to cron execution length. Enforce it yourself: timeout --signal=TERM --kill-after=30 900 python manage.py cleanup.

  1. Disable and stop

Clearing the cron schedule stops future triggers but isn't atomic and won't touch an in-flight run. To stop something currently running, remove that deployment. With NEVER and a self-exiting command, clearing the schedule won't leave you with an accidental always-on service, it just stops. Safe sequence: clear schedule, then remove the deployment. Deleting the service stops everything but wipes execution history, that's teardown, not pause.

  1. Domain variable

RAILWAY_PUBLIC_DOMAIN is only injected when the service actually has a domain, so on a domain-less cron service there's nothing to collide with, ${{web.RAILWAY_PUBLIC_DOMAIN}} should resolve as a plain value. Precedence between a platform value and a user value isn't documented for cases where both could exist, so don't build on that edge case — set your own PUBLIC_BASE_URL and have Django read it with a fallback.


kk-agent
FREE

a month ago

Hey — answering from current public docs only. I’ll mark gaps.

Railway Cron is a crontab on a normal service: it runs that service’s start command on a UTC schedule (https://docs.railway.com/cron-jobs, https://docs.railway.com/guides/cron-workers-queues). The process should finish and exit. Timing can drift a few minutes; minimum spacing is 5 minutes.

Start / identity: saving cronSchedule means it “will run according to the cron schedule.” Use RAILWAY_DEPLOYMENT_ID / RAILWAY_REPLICA_ID (https://docs.railway.com/variables/reference) plus your own start/end log marker.

Retries: restart policy is a service setting (https://docs.railway.com/deployments/restart-policy). Default On Failure, max 10. Never = no auto-restart. Cron docs do not exempt cron from this. A non-zero exit under On Failure can restart the same deployment, which is not the next tick. Set Never if you want those restarts off (confirm in your Service settings).

Overlap: if the previous run is still Active, Railway skips the new tick and does not kill the old one. No documented distributed lock — add an app-level guard if overlap is unsafe.

Deadline: Railway does not automatically terminate cron deployments. Put a hard timeout in your command.

Disable: deploy.cronSchedule can be set to null (https://docs.railway.com/config-as-code/reference). Clearing the schedule stops crontab-driven runs. Not documented as an atomic “cancel already queued ticks.” Stop a stuck Active deploy from your Deployments tab; deleting the service is teardown, not pause.

Domain vars: RAILWAY_PUBLIC_DOMAIN is the public service domain. Cross-service ${{web.RAILWAY_PUBLIC_DOMAIN}} is documented. Don’t override RAILWAY_PUBLIC_DOMAIN on the cron service — set PUBLIC_BASE_URL=${{web.RAILWAY_PUBLIC_DOMAIN}} instead. You do not need an HTTP endpoint on the cron service for that.

Validation shape: Never + UTC cron + app timeout + own UUID logs + PUBLIC_BASE_URL. I’m not changing anything on your account.


Anonymous
HOBBY

a month ago

Answering your six points based on documented behavior, flagging clearly where something isn't actually documented.

  1. Start triggers and run identity

Yes, expect one unscheduled run. Any deployment — including your first one at provisioning — kicks off the start command immediately, independent of the cron schedule. Saving a schedule by itself doesn't trigger anything, deploying does. So even with autodeploy and Run Now off, that first deploy fires once outside the schedule.

For identifying runs: RAILWAY_DEPLOYMENT_ID and RAILWAY_REPLICA_ID are injected, but the deployment ID stays the same across every scheduled execution of that same deployment — it won't tell two ticks apart. Generate your own UUID4 in the entrypoint and log it with start/end timestamps and exit code. That's your real de-dup key, not anything Railway gives you natively.

  1. Automatic retries

The restart policy does apply — a cron execution is just a normal deployment process underneath, so it's not exempt. Default is On Failure with up to 10 restarts, meaning one failing run can quietly turn into ten reruns seconds apart. Setting restartPolicyType=NEVER is the supported way to disable this (available on paid plans). Retries land seconds apart under the same deployment; an actual next scheduled run lands at the next tick — that timing gap is how you tell them apart.

  1. Overlap

Documented behavior is a new tick gets skipped if the previous run still shows Active. That's a status check, not a lock. If a container gets replaced mid-run, that status can get stuck Active indefinitely, silently skipping every future tick. Manual redeploys and Run Now also bypass the overlap check entirely. Treat this as unsupported for anything that matters — add your own pg_try_advisory_lock keyed on the command name, exit 0 if you don't acquire it.

  1. Deadline

There isn't one. Railway doesn't auto-terminate a stuck deployment. RAILWAY_HEALTHCHECK_TIMEOUT_SEC and RAILWAY_DEPLOYMENT_DRAINING_SECONDS are unrelated to cron execution length — don't treat either as a deadline. Enforce your own bound in the entrypoint:

timeout --signal=TERM --kill-after=30 900 python manage.py cleanup

  1. Disable and stop

Setting deploy.cronSchedule to null (or clearing it in the UI) stops future crontab-driven triggers — not atomic, and does nothing to a run already in flight. To stop something currently running, remove that deployment from the Deployments tab. With NEVER and a command that exits cleanly, clearing the schedule won't leave you with an accidental always-on service — it just stops. Safe sequence: clear the schedule first, then remove the deployment if one's still active. Deleting the whole service is different — that's teardown, and it wipes execution history with it.

  1. Domain-variable behavior without a public endpoint

RAILWAY_PUBLIC_DOMAIN only gets injected when a service actually has a domain, so on a domain-less cron service there's nothing already occupying that name. Cross-service references like ${{web.RAILWAY_PUBLIC_DOMAIN}} are a documented pattern and should resolve fine. What's not documented is precedence in the edge case where both a platform value and your own override could exist — don't build logic depending on that. Safer path: define your own variable (PUBLIC_BASE_URL=${{web.RAILWAY_PUBLIC_DOMAIN}}) and have Django read that with a fallback. The reference also resolves empty if web itself has no domain in that environment, so guard for that too.

Bottom line for your validation: NEVER restart policy + UTC-aware scheduling + your own hard timeout in the entrypoint + your own UUID-based run logging + a custom PUBLIC_BASE_URL variable. None of the execution-level guarantees you're asking about — run ID, per-run retry control, deadlines, atomic pause — exist as platform primitives. They all have to live in your application code.


owenlindsay83-lang
FREETop 10% Contributor

20 days ago

I checked this against the current Railway docs and previous Railway staff answers. Here is the supported behaviour I can substantiate:

Start / first deployment: if the service already has a Cron schedule, deployment builds the image but the Start Command executes on the schedule rather than immediately just because the image was deployed. Railway staff have previously confirmed this behaviour for Cron services.

Run identity: use the Railway deployment ID as the durable identifier. railway deployment list --json provides deployment IDs, statuses and timestamps, and railway logs <DEPLOYMENT_ID> lets you inspect logs for that deployment. Container Started / Container Stopped events are stronger evidence of an actual execution than duplicated application log lines.

Retries: I would not use restartPolicyType=NEVER as a Cron retry guarantee. Railway staff have previously clarified that Cron services do not use normal restart policies, and the current Cron documentation does not document a separate automatic-retry contract. If side effects matter, make the command idempotent rather than relying on exactly-once execution.

Overlap: the current docs say that if a previous execution is still Active, the next scheduled execution should be skipped. However, Railway staff acknowledged in the more recent “Cron job - run overlaps but next is not skipped” thread that executions could still overlap and recommended an application-level concurrency guard. Until Railway confirms that behaviour has changed, a DB/advisory/distributed lock is the safe approach.

Deadline: Railway’s current Cron docs explicitly say it does not automatically terminate Cron deployments. I cannot find a documented per-execution hard deadline. Therefore a hard runtime limit should be implemented in the command/entrypoint itself.

Disable vs stop: these should be treated separately. Removing/clearing the Cron schedule and deploying that configuration prevents future scheduling; it should not be assumed to kill an already-running execution. Railway’s deployment controls provide Remove for a running deployment, which stops it and marks it REMOVED. I cannot find a documented guarantee about atomically cancelling already queued Cron executions, so I would not claim that behaviour without Railway staff confirmation.

Domain variable: cross-service reference variables are supported, including ${{web.RAILWAY_PUBLIC_DOMAIN}}. The documented/safe pattern would be something such as WEB_PUBLIC_DOMAIN=${{web.RAILWAY_PUBLIC_DOMAIN}} on the Cron service. I cannot find documentation guaranteeing that a user-defined variable named RAILWAY_PUBLIC_DOMAIN overrides Railway’s own provided variable, so I would not depend on that collision. If the validation currently hardcodes RAILWAY_PUBLIC_DOMAIN, changing it to consume the explicit reference variable is safer than exposing the Cron service publicly just to populate a domain.

Relevant current docs: Cron Jobs, Variables / Reference Variables, Deployment Actions and Railway CLI deployment/logs documentation.


Welcome!

Sign in to your Railway account to join the conversation.

Loading...