A message queue that moves out of your way

Spooler is a simple, fully managed message queue.
So you can build your next big thing.

One two three, loop

Send message

# The body is the message: bytes in, bytes out, no envelope.
# 201 means stored; the queue "orders" was created beforehand.$ curl -si -X POST "$SPOOLER_API/spools/default/queues/orders/send" \
>   -H "Authorization: Bearer $SPOOLER_KEY" \
>   --data-binary 'hello, spooler!'HTTP/1.1 201 Created
Spooler-Message-Id: 1-1
Date: Wed, 21 Oct 2026 18:19:16 GMT
Content-Length: 0
$ 
# The CLI: go install github.com/spoolersh/spooler-cli/cmd/spooler@latest
# --data is the message, bytes as given, sent to the queue "orders"
# created beforehand. The reply is its id.$ spooler send --queue orders --data 'hello, spooler!'{
  "id": "1-1",
  "duplicate": false
}$ 
import "github.com/spoolersh/spooler-go"

// A client for the public API.
c := spooler.Client{
	APIKey: "...",
}
// The body is the message: bytes in, bytes out, no envelope.
// A successful send stores the message and returns its ID.
res, err := c.Send(ctx, spooler.SendRequest{
	Spool: "default",
	Queue: "orders",
	Data:  []byte("hello, spooler!"),
})
if err != nil {
	// Handle the error.
}
fmt.Println(res.ID) // 1-1

Receive message

# A receive does not delete the message: it leases it, and no other
# receiver gets it while the lease is live. -w captures it in $LEASE.$ LEASE=$(curl -si -X POST "$SPOOLER_API/spools/default/queues/orders/recv" \
>   -H "Authorization: Bearer $SPOOLER_KEY" \
>   -o /dev/stderr -w '%header{spooler-lease}')HTTP/1.1 200 OK
Cache-Control: no-store
Content-Length: 15
Content-Type: application/octet-stream
Spooler-Lease: AYUBAQIBAwEEAQUCI-tmOl2DyXcr3uUO
Spooler-Lease-Expires-At: 2026-10-21T18:24:16.875316003Z
Spooler-Message-Created-At: 2026-10-21T18:19:16.866018087Z
Spooler-Message-Id: 1-1
Spooler-Message-Retries: 0
Date: Wed, 21 Oct 2026 18:19:16 GMT

hello, spooler!$ 
# A receive does not delete the message: it leases it, and no other
# receiver gets it while the lease is live. -i shows the attributes
# ahead of the payload; --lease-file keeps the lease for the ack.$ spooler recv --queue orders -i --lease-file lease.json{
  "id": "1-1",
  "retries": 0,
  "createdAt": "2026-10-21T18:19:17.925683254Z",
  "lease": "AYUBAQIBAwEEAQUCfW9LB8QM8hLV7onb",
  "leaseExpiresAt": "2026-10-21T18:24:17.950929754Z",
  "dataSize": 15
}

hello, spooler!$ 
import "github.com/spoolersh/spooler-go"

// A client for the public API.
c := spooler.Client{
	APIKey: "...",
}
// A receive does not delete the message: it leases it, and no other
// receiver gets it while the lease is live; Ack presents it back.
msg, ok, err := c.Recv(ctx, spooler.RecvRequest{
	Spool: "default",
	Queue: "orders",
})
if err != nil {
	// Handle the error.
}
if !ok {
	// The wait elapsed with nothing to receive.
}
fmt.Printf("%q", msg.Data) // "hello, spooler!"

Ack the message

# Present the lease back. 204 means the message is acknowledged and
# removed from the queue. That is the whole loop: send, receive, ack.$ curl -si -X POST "$SPOOLER_API/spools/default/ack" \
>   -H "Authorization: Bearer $SPOOLER_KEY" \
>   -H "Spooler-Lease: $LEASE"HTTP/1.1 204 No Content
Date: Wed, 21 Oct 2026 18:19:16 GMT
$ 
# Present the lease back. ack prints nothing: exit 0 means the message
# is acknowledged and removed. That is the whole loop: send, receive, ack.$ spooler ack --lease-file lease.json$ 
import "github.com/spoolersh/spooler-go"

// A client for the public API.
c := spooler.Client{
	APIKey: "...",
}
// Present the lease back: the message is acknowledged and removed
// from the queue. That is the whole loop: send, receive, ack.
err := c.Ack(ctx, spooler.AckRequest{
	Spool: "default",
	Lease: msg.Lease,
})
if err != nil {
	// Handle the error.
}

Simple for humans, obvious for agents

Spooler gives your agent the tools and the skills to work the queue. If your agent can run a command, it can use Spooler.

  • Hand it the CLI

    spooler help documents the commands, flags and exit codes, with links to the docs. Your agent reads the same help you do, and the skills explain how to use Spooler.

  • Let it work the queue

    Your agent is a worker like any other: it leases messages and acks them when done. A crash costs a redelivery, not the message. You or your agent can inspect failed messages, then recover or discard them.

  • Verify the results

    You and your agent use the same commands: data on stdout, diagnostics on stderr and documented exit codes. You can list messages, inspect a payload or describe a spool with the same CLI.

Where it fits

State machines

Your queues can model a state machine: each queue a state, each message an object in that state, and ack-and-send the transition. It acks the current message and appends the next in one atomic call. Both changes happen, or neither does.

[spooler] 0:pipelines*

Webhooks

Your handler sends each webhook to the queue and returns 200 once the send succeeds. Bursts wait in the queue while your workers process them at their own pace.

[spooler] 0:webhooks*

Third-party APIs

Your queue paces work to match a third-party API's limits. Its receive rate and burst size control how fast your workers receive messages. All workers share that limit.

┌─ in ───────────────────────────────── 58/s ─┐
│         ▁▁                                  │
│      ▁▄▆██▇▄▂                               │
│█████▇████████▇▇▇▇▇▇▇▇██▇█▇▇▇▇▇▇▇▇▇▇▇▇▇█▇▇▇▇▆│
└─────────────────────────────────────────────┘
┌─ out ──────────────────────────────── 40/s ─┐
│                                             │
│                                             │
│▅▅▅▅▅▅▅▅▅▅▅▅▅▅▅▅▅▅▅▅▅▅▅▅▅▅▅▅▅▅▅▅▅▅▅▅▅▅▅▅▅▅▅▅▅│
└─────────────────────────────────────────────┘
┌─ queue ─────────────────────── 189 (5s) ─┐
│                   ▁▁▁▂▂▂▂▂▃▃▃▃▄▄▄▅▅▅▅▅▆▆▆▆▇▇│
│     ▁▂▄▅▆▇▇▇▇███████████████████████████████│
│▇████████████████████████████████████████████│
└─────────────────────────────────────────────┘

┌─ recvRateLimit ──────────────────────────┐
│                                             │
│ rate     [━━━━━█━━━━━━━━━━━━━━━━━━━━━] 40/s │
│ burst    [━━━━█━━━━━━━━━━━━━━━━━━━━━━]    8 │
│                                             │
└─────────────────────────────────────────────┘
┌─ input ──────────────────────────────────┐
│                                             │
│ rate     [━━━━━━━━█━━━━━━━━━━━━━━━━━━] 60/s │
│ burst    [━━━━━━━━━━━━━█━━━━━━━━━━━━━]   40 │
│                             │
└─────────────────────────────────────────────┘
[spooler] 0:simulation*

Worker pools

Your workers take background jobs from one queue. Each job goes to one worker at a time, under a lease. If that worker crashes, the lease expires and the job becomes available to another worker.

[spooler] 0:worker-pools*

The clever bits that keep it boring

  • Isolation

    A spool is the storage unit for your queues. It has its own log, write buffers and replication, and handles its I/O independently. A full spool refuses new sends rather than letting a backlog increase your bill.

  • Durability

    Your send succeeds only after the message is written and synced to disk and the required replicas have confirmed it. Acks and atomic ack-and-send follow the same durability rule.

  • Leases

    A receive leases a message rather than removing it. You ack when the work is done, or renew if it takes longer. Delivery is at-least-once, so a crash mid-work costs a redelivery, not a message.

  • Deduplication

    When a connection drops, you retry the send with the same dedup key. Within your queue's dedup window, that retry adds no second message.

  • Atomic ack-and-send

    Your worker acks one message and appends the next in one atomic call: both happen, or neither does. If it retries a call that already committed, nothing changes, so the hop happens exactly once.

  • Failed messages

    A message that keeps failing doesn't block your queue. When its retries run out, or your handler marks it failed, it enters the failed state with its payload intact. You can inspect it, then recover or discard it.

Pricing

One price a month. If your spool fills up, Spooler rejects new sends until your workers make room. Stalled workers do not mean a surprise invoice.

  • Solo

    £29 / month

    60 days trial — no card required

    • 1 spool
    • 250 req/s
    • 7 days retention

    Per spool:

    • 1,000 queues
    • 100,000 stored messages
    • 2 GiB stored data
    • 64 KiB per message
    • 1 replica
  • Team

    £149 / month

    45 days trial — card required

    • 2 spools
    • 500 req/s
    • 14 days retention

    Per spool:

    • 10,000 queues
    • 250,000 stored messages
    • 4 GiB stored data
    • 64 KiB per message
    • 2 replicas
  • Business

    £299 / month

    30 days trial — card required

    • 3 spools
    • 1,000 req/s
    • 30 days retention

    Per spool:

    • 100,000 queues
    • 500,000 stored messages
    • 8 GiB stored data
    • 64 KiB per message
    • 3 replicas

Behind the queues

Bootstrapped
Spooler is owned and run by its founder. Pricing is designed to support running and maintaining the service with a small customer base.
Managed service
Spooler is a paid, fully managed service. The server code is closed source, with no self-hosted edition. You can evaluate the service with a free trial and use the local image free for development and testing.
If Spooler closes
You get at least 90 days' notice by email. You can drain your queues through the same API you already use.
Where it runs
Dedicated servers in Falkenstein, Germany, operated by Hetzner. The data policy says what is stored where.

Creativity needs space