Skip to main content

Railway Edge Rules vs a Gateway You Own: What Each Rule Costs to Replicate

14 min readDora NodaDora Noda
Share
On this page

One Railway user's commit message tells you everything about the state of edge policy in 2026: "Edge rules have no CLI, so the scanner-floor ruleset is docs/edge-rules.json to paste." The team keeps its firewall rules in Git — and then hand-pastes them into a console. That file, and the paste, is the whole story: Railway's Edge Rules are genuinely useful request policy (allow, block, challenge, redirect, cache override, evaluated at the vendor edge before traffic reaches your service), and they live entirely outside your deployment pipeline.

So here is the question this post answers, rule by rule: if you leave Railway, or never adopt it, what does each of those five rules cost to rebuild on a gateway you own? The mapping table first, then the receipts.

The mapping, up front

Edge Rules actionSelf-hosted equivalentEffortFleet-rollout note
Redirect (301/302/307/308, path/query preserved)Gateway API RequestRedirect filterAn afternoonOne HTTPRoute in Git; every cluster converges on apply
Block (custom 400–499 + body)Envoy direct_response route or implementation equivalent; no standard Gateway API filterAbout a dayOne shared deny policy, attached per route
Allow (ordered exception above a block)Same mechanism, inverted: RBAC allow / SecurityPolicy IP allowlist with deny defaultHours, once block existsException lists become reviewable YAML instead of console rows
Challenge (browser verification)Anubis (self-hosted proof-of-work proxy) or ext_authz + captcha serviceA week or moreA new hop in every cluster's request path, tuned per tenant
Cache override (fixed TTL 1s–30d, or bypass)A cache layer you own: cache filter or CDN + Cache-Control from app/gatewayDays to stand up, ongoing tuningTTL policy becomes config you version, not a console toggle

Two findings are worth your time even if you skim nothing else. First, the cheap rows are cheap because they are standard: redirects map onto a Gateway API core filter nearly line for line. Second, the expensive row — challenge — is expensive in a way no YAML snippet fixes, because interactive verification is a running service with false positives, not a route rule. Everything below earns each cell.

What Edge Rules actually are (and what the matchers cost)

Railway announced Edge Rules in its August 14, 2026 changelog. The model is small: rules are configured per service per environment, apply to every domain on that service, and evaluate at the nearest edge location before the request reaches your workload. Evaluation is priority-ordered with first-terminal-action-wins (block, allow, challenge, redirect are terminal; cache_override applies and continues), and saves propagate worldwide within seconds. A plan allowance gates rule count, and a public domain must be attached before the editor unlocks.

Conditions match four attributes — client IPv4/CIDR, host, path, header — combined with and/or nesting (plus not in JSON mode), with * wildcards under matches. Limits are published and tight: a 64 KiB ruleset, 32 clauses per rule, four nesting levels, 64 entries per in list, eight wildcards per pattern. Edit-as-JSON mode with whole-ruleset validation is why teams can keep a railway-edge-rules.json in the repo at all — one public ruleset from late August gates /api/routes to a trusted IP and blocks everything else.

The matchers deserve their own cost row, because "evaluate IP, host, path, header" sounds trivial until you re-implement the semantics:

Edge Rules matcher semanticSelf-hosted equivalentCost
IPv4/CIDR, host, path, header conditionsHTTPRoute matches (path, headers, query) + policy CIDR listsNative; an hour to transcribe
and/or/not nesting, 4 levels deepRule ordering + multiple routes; deep nesting gets flattened by handThe transcription is the cost — no nesting primitive
Priority order, first terminal winsGateway rule precedence (first match wins in most implementations)Free where semantics align; audit where they don't
JSON mode with whole-ruleset validationYAML in Git with kubectl --dry-run + admission policyStrictly better: validation and history
Worldwide propagation in secondsxDS push in seconds + GitOps reconcile in minutesSlower, but every change is a reviewed diff with a rollback

Note the sharpest matcher limit, straight from Railway's docs: client-IP matching is IPv4-only — "requests without an IPv4 source don't match." Any allowlist you build on it silently ignores IPv6 clients. A self-hosted RBAC policy with v6 CIDRs is not merely equivalent here; it closes a hole the vendor rule cannot see.

Redirect: the nearly-free rule

A Railway redirect rule names exactly one of host or location, an optional status from 301/302/307/308, and preserve_path/preserve_query booleans defaulting to true. The Gateway API equivalent is the RequestRedirect core filter, and the field mapping is almost mechanical:

yaml
filters:
  - type: RequestRedirect
    requestRedirect:
      hostname: "new.example.com"
      path:
        type: ReplaceFullPath
        replaceFullPath: /landing
      statusCode: 301

host becomes hostname, location becomes a hostname-plus-path combination, and preserve_path: false with a replacement maps onto ReplaceFullPath or ReplacePrefixMatch. The one field with no explicit toggle is preserve_query: in practice most implementations carry the query string through a redirect, but that is implementation behavior to verify with a conformance check, not a spec promise you can rely on blind.

The status-code caveat is real but bounded. Gateway API's redirect enum was 301/302-only for years; 303/307/308 were added in May 2025 as Extended support, which means an implementation may legally ignore them. If your ruleset uses 307s to preserve POST bodies across a redirect — the one case where 307-vs-302 is load-bearing — confirm your implementation honors Extended codes before you declare parity. Effort rating: an afternoon, including the conformance check. This is the cheapest row in the table because both sides implement the same ten-year-old HTTP semantics.

Block: custom status, no standard filter

Railway's block returns any status from 400 through 499 (default 403) with an optional body up to 4 KB. This is the bouncer row: scanner-floor denies, /admin lockdowns, gone-away endpoints that should answer 410 instead of 404.

Here the standards story inverts. Gateway API has no standard "respond directly" filter — a DirectResponse filter has been a requested-but-open spec issue for years — so every implementation rolls its own: Contour has one, Envoy exposes direct_response on the route, and policy layers add their own deny actions. A portable-enough shape on Envoy-family gateways is a route that answers without a backend:

yaml
# Conceptually: match the denied traffic, answer 410 + body, no backend.
# Spelling varies by implementation (direct-response extension,
# policy deny action); the route match itself is standard HTTPRoute.

That "spelling varies" is the entire cost of this row: the match is portable, the answer action is implementation-specific, so a fleet standardizing on one gateway pays it once and tenants reuse it. Budget about a day to pick the spelling, prove status code and body byte-for-byte against the Railway rule, and wrap it in a shared policy.

Ongoing list maintenance costs the same on both sides — denied CIDRs, scanner paths, and deprecated endpoints rot either way. The difference is where the rot lives: a console list nobody diffs, or YAML that shows up in pull requests.

Allow: the exception that must sort first

Railway's allow forwards to the service and skips all later rules; the documented pattern is an allow rule matching /admin* plus the office CIDR, sorted above a broader /admin* block. It is terminal-admit: the exception that must evaluate before the rule it punches through.

On your own gateway this is the same mechanism as block, inverted — and that is why it is a separate row with a smaller price tag. An Envoy Gateway SecurityPolicy with an IP allowlist and a deny default, or a plain RBAC allow-with-default-deny, reproduces the semantics; ordering falls out of route precedence instead of drag-to-sort. The transcription cost is hours given the block row already exists, and the genuine improvement is IPv6: Railway's allow rule cannot admit a v6 office network it cannot match, while your RBAC list can carry both families side by side.

One subtlety survives translation: Railway's "negative comparison against a missing value doesn't match" (a header is-not X rule ignores requests lacking the header entirely). Most gateway match semantics agree, but this is exactly the kind of edge that deserves one negative test case per translated rule — matching the absence behavior, not just the presence behavior.

Challenge: the expensive row

Railway's challenge shows a browser verification page to visitors without clearance; cleared visitors continue through later rules on subsequent requests. This is bot-floor defense with a human escape hatch, and it is the row where "equivalent YAML" stops being the right question, because interactive verification is a service, not a route rule. Cost it on four dimensions:

  • Setup. Self-hosting means running the challenger as a hop in the request path: Anubis — the open-source proof-of-work reverse proxy behind much self-hosted AI-scraper defense — sits in front of the origin issuing Hashcash-style puzzles browsers solve in JavaScript. Deploying it per cluster (or per route, via ext_authz callout) is days of plumbing before the first rule exists.
  • Tuning and false positives. Railway tunes its verification page; you tune puzzle difficulty. Too easy and scrapers amortize it; too hard and low-end devices time out. Somebody on your team now owns the "legitimate user can't get in" queue.
  • UX and latency. A solved proof-of-work adds sub-second JavaScript execution per fresh client. Comparable to a vendor interstitial in the common case, worse on constrained devices — measure on a real phone, not a datacenter VM.
  • Maintenance. Challenge efficacy decays as solver tooling improves; the vendor ships that arms race invisibly, while your Anubis version, difficulty settings, and bot-signature rules need a cadence with an owner.

Budget a week minimum to reach parity with one Railway challenge rule, and treat parity as a moving target. (Railway's separate WAF Under Attack Mode — CLI-managed, ~20-second propagation, CAPTCHA-style, incident-scoped — is a per-service panic button that turns away all non-browser traffic including API clients, which is precisely why a path-scoped challenge rule exists alongside it. Don't confuse the two when translating.)

Cache override: the layer you were always going to own

Railway's cache_override is the only non-terminal action: cache matching responses for a fixed TTL (1 second through 30 days) or bypass the edge cache, then continue evaluating later rules. The canonical pair is "cache /assets/* for a day, bypass /api/* entirely."

There is no Gateway API standard for this — caching is not a route concern in the spec — so the mapping lands one layer down, on infrastructure you pick and operate:

yaml
# The portable half: the app (or a header-modifier filter) declares policy,
# and the cache you own enforces it.
#   Cache-Control: public, max-age=86400        # ttl_seconds: 86400
#   Cache-Control: no-store                     # bypass: true

TTL maps onto max-age honored by your cache (an Envoy cache filter, a Varnish/nginx tier, or a CDN in front of the fleet); bypass maps onto no-store or a route the cache skips. The snippet is small because the policy is small — the cost is standing up and sizing the cache (days), plus the tuning Railway's CDN team does for you: hit-ratio monitoring, stampede protection on expiry, purge discipline when content changes early. Note the failure modes at the range ends: a 1-second TTL is barely caching, and a 30-day TTL without a purge path is a content freeze. Reproduce the TTL, but also the purge story the console never asked you to think about.

The sixth row that doesn't exist: rate limiting

Count the table again: five Railway actions, all mapped. Now notice what is missing from Railway's side entirely — there is no rate-limit action in Edge Rules. No per-client throttle, no per-path quota; the documented action set is allow, block, challenge, redirect, cache override, full stop.

This is the row where the self-hosted side is not catching up but ahead. An Envoy Gateway BackendTrafficPolicy expresses global (Redis-backed, fleet-wide) and local (per-instance, cheap) rate limits with header- or CIDR-based buckets, answering 429 with x-envoy-ratelimited when a bucket empties. "100 requests/second globally, 5 signups/minute per client on the signup route" is a few lines of YAML — policy Railway's edge cannot express at any price. If your migration motivation includes the abuse case Edge Rules was bought for, this is the upgrade you schedule in the same window: translate the five rows you had, then add the sixth row you always wanted.

The quiet lock-in: policy that lives outside Git

None of the above is hard because the rules are powerful — a five-action ruleset is a weekend's worth of YAML. The lock-in is duller: Railway's rules live in a console, scoped per service per environment, behind a plan allowance, with no CLI. The evidence is user behavior the docs quietly bless — teams committing railway-edge-rules.json to the repo and pasting it into Edit-as-JSON, because paste is the only deploy pipeline the feature offers. The JSON is portable; the workflow is not: no diff review, no CI gate, no git log of who changed the /admin allowlist, no rollback beyond "paste the old JSON back."

That shapes the migration checklist more than any single action mapping:

  1. Export the portable artifact. Copy each service's ruleset out of Edit-as-JSON into Git — per service, per environment. The paste habit accidentally did the right thing; formalize it.
  2. Translate row by row using the mappings above, and keep the Railway JSON beside the YAML until the negative test cases pass (especially absent-header behavior).
  3. Re-scope deliberately. Per-service-per-environment console rules usually collapse into a small set of fleet defaults plus per-tenant overrides — one place the office-CIDR allowlist lives instead of N consoles.
  4. Re-own propagation. Console saves land in seconds; a GitOps apply lands in minutes but arrives as a reviewed diff with an author, a reason, and a revert. That trade is the point, not a concession.
  5. Add the sixth row. You are touching every route anyway; this is the cheapest moment rate limiting will ever be.

Count the fleet-wide distribution cost honestly in step 3: one GitOps apply converging every cluster versus per-service console edits multiplied by environments. At one service and one environment the console wins on speed. Past a handful of either, the console cost scales with clicking and the Git cost stays flat — which is the same crossover math as every other piece of infrastructure that ever moved into code.

Verdict: replicate the cheap rows, deliberate the expensive one

Translate redirects first (an afternoon, nearly mechanical), block/allow second (a day, and take the IPv6 win), and cache policy third (days, and design the purge story the console never made you write). Challenge is the only row that deserves a build-vs-tolerate decision: Anubis is a real, production-used answer, but it is a service with an owner and a tuning cadence, not a rule you transcribe — adopt it because you want its control, not because the translation looked free. And take the rate-limit upgrade the vendor side never offered you.

The deeper lesson is the paste. Any policy your team maintains by copying JSON out of a console is policy that has already outgrown the console — it wants to be code, with review, history, and rollback. Edge request policy got there like everything else did: first as a vendor feature you click, then as a file you paste, and finally as a manifest you apply. Skip the middle step on your own fleet.

Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own. Star the repo on GitHub or deploy your first app today.

Sources

Related articles

Run this on infrastructure you own

bex is the open-source, AI-native Render alternative — push a git repo and get a running HTTPS service on your own machines.

Get started with bex