Enqueue work from Vercel and run customer-owned workers on Railway or another host, with scoped keys and recoverable execution.
Durable background jobs for Vercel applications
A request handler is a good place to accept a user action and enqueue work. It is a poor place to run an indefinite worker polling loop. Function execution has a bounded lifecycle; returning a response does not create a durable process, and a request-bound timer or an unawaited promise is not a job queue.
GetRatchet separates the producer, control plane and worker. A Vercel application submits a versioned operation. GetRatchet persists the job, coordinates leases and records attempts. A customer-owned TypeScript or Python worker executes the handler on infrastructure you choose. GetRatchet does not host your handler code, and it does not require Railway.
Enqueue from the server, not the browser
Keep the INGEST key in a server-only environment variable. Authenticate and authorize the user's action in your application before enqueueing. Do not let an arbitrary browser choose a privileged handler or construct unbounded payloads.
import { createRatchet } from '@getratchet/sdk';
// Called only after application authentication, authorization and input validation.
export async function enqueueWelcome(customerId: string) {
const ratchet = createRatchet({
baseUrl: 'https://getratchet.app',
apiKey: process.env.GETRATCHET_INGEST_KEY!,
});
await ratchet.startRun('Welcome email');
const job = await ratchet.enqueue({ customerId }, {
name: 'send_welcome_email', version: '1',
idempotencyKey: `welcome:${customerId}:v1`,
maxAttempts: 5, timeoutMs: 30_000,
});
await ratchet.finishRun();
return job;
}Await enqueue acceptance before returning success to the caller. Requesting run completion does not synchronously execute the handler: the run stays pending until its required work completes. If your producer request can repeat, keep a stable business identity and use destination-side idempotency too. Creating a new run does not make every external side effect globally unique.
Deploy the customer-owned worker
The repository provides examples/railway-worker, a Docker-based Node worker starter with a health endpoint and shutdown handling. The public starter repository is getratchet-worker-starter. The application's onboarding flow links to the configured Railway template when one is available; otherwise it opens the customer-worker deployment guide. Do not assume the template contains your business logic: the example handler is getratchet_echo@1.
Use your own Railway account or another host capable of keeping the worker process alive. Worker compute and destination API costs are separate from GetRatchet subscription charges. A VM, container service or another suitable process host can run the same SDK polling loop.
Create a WORKER key scoped to the correct project and development, staging or production environment. Allowlist exact handler names and versions. Keep the INGEST key in the producer; do not give the worker an ADMIN key. Set the starter's RATCHET_BASE_URL to https://getratchet.app, RATCHET_WORKER_KEY to that scoped key and RATCHET_WORKER_ID to a stable unique ID for one replica. Store destination credentials only in the worker's environment.
When scaling replicas, assign distinct worker IDs and bound concurrency against provider limits. Register all handler versions still needed by queued jobs. Replacing version 1 with version 2 without leaving a compatible worker can strand older work; deployment of new code is not an automatic job-version migration.
Plan for shutdown and uncertainty
Stop polling on shutdown and give active handlers time to finish. If a process disappears, lease expiry allows another worker to reclaim work. A stale report cannot overwrite a newer lease, but the old process may already have made a remote call. The idempotency guide explains that remaining duplicate risk.
Propagate the TypeScript context's abort signal to HTTP calls. In Python, check cancellation and use bounded network timeouts. Cancellation is cooperative; it cannot undo an email or database write already accepted by another service.
Before production traffic, enqueue a disposable echo operation and inspect its successful attempt. Then verify the real handler's input validation, non-retryable failures, transient-provider retry behavior and graceful shutdown in an isolated environment. If jobs remain queued, check worker liveness, exact handler version, key scope, endpoint pause, circuit state and queue gates before increasing polling frequency.
See worker operations, the Python worker SDK, and circuit gates versus retry timing. The pricing comparison explains attempt allowances and retention; a background architecture does not imply unlimited throughput or a latency SLA.