Skip to content

Background & Workers

Weld runs work off the request path with three primitives, plus the periodic every timer:

  • startup { } — a boot hook that runs once, before the server accepts traffic.
  • worker [name] { } — a background thread that runs until it returns or the server shuts down. Paired with a topic, a worker is a job queue.
  • spawn <call> — fire-and-forget a single upstream/database/gRPC call from a handler.

startup

Runs once at boot, synchronously, before the first request is served — seed data, warm a cache, log configuration. It runs single-threaded, so it needs no locking.

weld
state cache { ready: bool = false }

startup {
    cache.ready = true          # no other thread is running yet
}

worker — background loops and job queues

A worker body runs on its own thread at startup. On its own it can do periodic-free background work, but its real power is consuming a topic: handlers publish, the worker processes off the request path.

weld
type Job = { id: int, url: string }
topic Jobs -> Job

worker {
    for j in Jobs {             # blocks for the next job; ends on shutdown
        crawl(j.url)            # do the slow work here, not in the handler
    }
}

route POST "/crawl" (body j: Job) -> string {
    Jobs.send(j)                # returns instantly
    respond "queued"
}

A worker's topic loop is shutdown-aware (it ends when the server drains), reclaims each message's memory, and — because it runs alongside request handlers — locks shared stateper message, never across the whole loop. A single worker per topic processes each job once; running several workers on one topic would each see every job (topics are broadcast).

spawn — fire-and-forget a call

When you just want to fire one outbound call and not wait for it — a webhook, an email, an audit write — spawn runs it on a background thread and returns immediately:

weld
route POST "/signup" (body u: User) -> string {
    db.insertUser(u)                 # awaited: must succeed before we respond
    spawn mailer.sendWelcome(u.email)  # fire-and-forget: don't block the response on it
    spawn analytics.track(u.id)
    respond "created"
}

spawn takes an upstream, database, or gRPC call. Its arguments are deep-copied before the thread starts (so they safely outlive the request), the call runs on a background thread, and its result is discarded. Failures are swallowed — if you need the result or retries, use a worker + topic instead.

Cancellation

startup completes before serving; worker loops end on shutdown (with the normal connection drain). spawn tasks are best-effort — in-flight ones may be cut off when the process exits. Don't spawn work that must not be lost; publish it to a topic a worker drains. Background blocks run on the threaded driver (not --green).