DreamLake

Queue model

A Lakeshore queue is one record carrying two jobs. It is a task queue — the thing invocations line up in and workers pull from — and it is a launch queue — the policy deciding how many workers should exist. They are one model because the depth of the first is the input to the second.

The queue name you write in @udf(queue="gpu") is denormalized onto every invocation, and the control plane reads that one string twice: once to route a claim to a worker, and once to count how many invocations are still waiting. Counting the backlog is how the autoscaler knows whether to grow the pool. Splitting scheduling and provisioning into separate objects would mean keeping that number in sync across both.

Read the two halves separately:

  • Task queue — the behaviors: submit, claim, complete, retry, stream.
  • Elastic queues — what attaching a provider adds.

The type

Queuets
interface Queue {
  id:            string
  namespaceId:   string          // the prefix — see below
  name:          string          // unique within namespaceId
  description?:  string

  // ── Scheduling surface ────────────────────────────────────────────
  kind:          'fifo' | 'priority' | 'boltzmann' | 'filo'   // default 'fifo'
  kindParams?:   { temperature?: number; priority_field?: string }
  admission?:    {
    max_depth?:          number
    on_full?:            'reject' | 'block'
    rate_limit?:         number
    deadline_cutoff_s?:  number
  }
  reservedSlots?: number         // advisory slots held back
  authPolicy?:    unknown        // opaque blob; consumer is the auth gate

  // ── Launch policy ─────────────────────────────────────────────────
  providerRef?:   string | null  // provider name; null = bring your own worker
  elasticity?:    {
    kind:        'fixed' | 'fully_elastic' | 'pool_with_threshold' | 'max_count'
    min?:        number
    max?:        number
    threshold?:  number
  }
  daemonTemplate?: {
    instance_type?:  string
    image_id?:       string
    runner?:         string
    setup_scripts?:  string[]
  }
  customScalerUdf?: string       // optional UDF overriding the built-in scaler

  // ── Lifecycle ─────────────────────────────────────────────────────
  state:         'active' | 'draining' | 'paused' | 'archived'   // default 'active'
  serverNodeId?: string | null   // owning queue-server tier; null = central

  createdAt:     Date
  updatedAt:     Date
}

Naming and the prefix

A queue name is never global. The record carries a namespaceId, and the uniqueness constraint is on the pair:

@@unique([namespaceId, name])

So the namespace is the prefix. Two namespaces can both own a queue called gpu and they are different queues; within one namespace the name is the identity, which is what lets @udf(queue="gpu") be a bare string in user code — the namespace comes from the caller's credentials, not from the literal.

Everything keyed off a queue inherits that scoping. The idempotency key on a submit (_key=) is unique per namespace + queue, not per queue name alone, so the same key in two namespaces is two distinct invocations.

The prefix is the namespace, not a name segment

There is no separate prefix field, and name is a flat string — Lakeshore does not parse team/gpu into a hierarchy today. If you want grouping inside a namespace, it lives in the name by convention and nothing enforces it.

This differs from a Redis-backed queue like zaku, where the prefix is a literal key prefix (Zaku-task-queues:{queue}:pending) chosen at client construction. In Lakeshore the equivalent partitioning is a control-plane relation, not a string you assemble.

Creating one

terminalbash
lakeshore queues add gpu-train \
  --kind priority \
  --provider aws-east \
  --elasticity fully-elastic --min 0 --max 4

You rarely have to: submitting to a queue name that does not exist creates it, with kind defaulting to fifo. Reach for queues add when you want a non-default discipline or a launch policy.

Two spellings for the elasticity kinds

The CLI takes the dashed forms above (fully-elastic); the REST API and compose files take underscores (fully_elastic). The CLI translates between them.

Stored ahead of enforced

Several fields above are persisted and validated but not yet acted on, and the gaps are not visible from the field names:

FieldReality today
kindThe hosted control plane dispatches by priority then oldest-first regardless of the value. The four disciplines are implemented in the SDK's local plane.
admissionValidated and persisted, not applied at submit time.
reservedSlotsCarried over from the collapsed Lane model; no scheduler reads it.
customScalerUdfReserved for a later phase; the built-in scaler is the only one that runs.

Check the reference before designing around any single field.

Task queue →

The behaviors: submit, claim, complete, retry, stream — and the state a task moves through.

Elastic queues →

What providerRef adds: scaling policies, the controller tick, and the event feed.

Queues reference →

The full CLI surface and the current state of each field.

Providers →

What providerRef points at, and which provider types can launch today.