⚡ PulseMQ

Send it now. Send it later. One queue.

Durable message queue with native scheduled delivery. Self-hosted. Zero dependencies.

Get started in 60s → View on GitHub

v1 · single-node  ·  3-node Raft cluster support is on the roadmap

Schedule a message with curl
# Schedule a payment reminder in 24 hours curl -X POST "http://localhost:8080/namespaces/payments/queues/reminders/messages" \ -H "Content-Type: application/json" \ -d "{\"body\":\"eyJ1c2VySWQiOjEyM30=\",\"deliver_at\":$(( $(date +%s%3N) + 86400000 ))}" # Response: {"id":"01JP5X4PRJ3G2V9WB8E7KT8M5F"} # Delivery: at or after the given UTC millisecond timestamp.
O(1) scheduled message peek
~8 MB Docker image
±15 ms delivery accuracy (Linux)
0 runtime dependencies
~15k msgs/sec (single node)

Why PulseMQ

Traditional scheduling needed workarounds.
PulseMQ makes scheduling first-class.

Until now, delayed and time-based workflows usually required multiple moving parts. PulseMQ brings them into one simple queueing model.

❌ Traditional approach

  • Separate scheduler jobs and queue workers to maintain
  • Frequent polling loops that grow cost with load
  • Multiple services and extra operational complexity
  • Delivery windows and timing limits in delay workflows
  • Language- or stack-specific tooling constraints
  • More glue code for retries, timeouts, and reliability

✅ The PulseMQ way

  • Native deliver_at — any future UTC millisecond
  • Configurable scheduling window (default 90 days)
  • Language-agnostic HTTP + WebSocket API
  • Single binary, zero dependencies, ~8 MB image
  • Runs on 512 MB RAM
  • Durable WAL + bbolt index — survives restarts

Why PulseMQ instead of a separate scheduler service?


Alternatives

How PulseMQ compares.

PulseMQ is purpose-built for scheduled and delayed delivery. Here's how it stacks up against common alternatives.

Capability PulseMQ Redis + BullMQ Sidekiq Temporal
Native ms-precision scheduled delivery ✅ first-class ⚠️ delayed jobs (polling) ⚠️ scheduled jobs (polling) ✅ workflow timers
Self-hostable, single binary ✅ ~8 MB, zero deps ⚠️ requires Redis ⚠️ requires Redis + Ruby ⚠️ multi-service cluster
Clustering ❌ v1 single-node
Raft on roadmap
✅ Redis Cluster ✅ via Redis ✅ built-in
Language support Any (HTTP/WS) + Go SDK JS / TS Ruby Many official SDKs
Dead-letter queue ✅ built-in per queue ✅ failed job set ✅ retry + dead set ✅ workflow history
Operational complexity Low — one process Medium — Redis + workers Medium — Redis + Sidekiq High — server + workers + DB
Primary use case Scheduled & delayed job delivery General background jobs (Node) Background jobs (Ruby) Long-running, stateful workflows

PulseMQ is the right choice when you need precise scheduled delivery with minimal operational overhead and no external dependencies. For complex multi-step orchestration, Temporal may be a better fit.


Features

Everything a production queue needs. Nothing it doesn't.

Built ground-up in Go. No dependencies, no configuration sprawl.

Native scheduled delivery

Set deliver_at to any future UTC millisecond. An in-memory Min-Heap (O(1) peek, O(log N) insert) fires messages precisely when due — no database polling loop.

🔒

At-least-once delivery

Visibility timeouts lock messages while in-flight. If your consumer crashes without ACKing, the message becomes ready again automatically.

💀

Dead-letter queue

Every queue gets a paired DLQ. Failed messages land there after max_retries. Inspect and replay with a single API call.

🌐

Three consumer models

HTTP poll, WebSocket push (with backpressure), or Webhook delivery — pick what fits your architecture. Mix and match per queue.

📦

Durable storage

Append-only WAL + bbolt index. On restart the WAL is replayed to restore all state exactly as it was. Default fsync=interval (1 s) — for zero data loss on a hard crash set fsync=always. Durability details →

📊

Built-in dashboard

Live queue depths, scheduled counts, DLQ alerts, pagination for 500+ queues. Create queues, send messages, replay DLQ — all from the browser.

PulseMQ dashboard
🔭

Prometheus metrics

Published, consumed, ACKed, NACKed, DLQ-routed counters per queue. HTTP request latency histograms. Scrape from port 9090.

🔑

Auth + rate limiting

Static API key via X-Api-Key header. Per-IP token-bucket rate limiter and request body size cap built in.

🚀

Single-node v1 · Raft on the roadmap

v1 runs as a single node. 3-node Raft cluster support is on the roadmap — and the WAL format already carries Raft term + log_index fields, so the storage format won't change when clustering ships.


"Send email 1 hour after signup? Schedule a payment retry in 24 hours? Cancel an order in 30 minutes? That's one deliver_at."
— Stop writing cron jobs. Start publishing messages.

How it works

Min-Heap scheduling.
O(1) peek. Always.

Traditional solutions poll the database every second. PulseMQ knows exactly when the next message is due and sleeps until that moment.

1

Publish with deliver_at

Set deliver_at to a UTC millisecond timestamp. If it's in the future, the message enters the scheduler's Min-Heap. If immediate, it goes straight to the READY queue.

2

Scheduler fires at exactly the right time

A background goroutine peeks at the heap root (O(1)) and sleeps until that timestamp. On fire, it pops the message and pushes it to READY.

3

Consumer receives the message

Your consumer polls, subscribes via WebSocket, or receives a webhook POST. The message is locked with a visibility timeout.

4

ACK or retry

Call DELETE /messages/:receipt_handle to ACK. Call POST .../nack to requeue. Exhaust retries and it moves to the DLQ.

Go SDK Go
import "github.com/sneh-joshi/pulsemq/pkg/client" c := client.New("http://localhost:8080") // Publish immediately id, _ := c.Publish(ctx, "payments", "invoices", payload) // Schedule in 1 hour id, _ = c.Publish(ctx, "payments", "invoices", payload, client.WithDelay(time.Hour)) // Schedule at an exact time id, _ = c.Publish(ctx, "payments", "invoices", payload, client.WithDeliverAt(monday9am)) // Consume + ACK msgs, _ := c.Consume(ctx, "payments", "invoices", 10) for _, m := range msgs { process(m.Body) c.Ack(ctx, m.ReceiptHandle) }
Python (requests) Python
import requests, base64, json, time BASE = "http://localhost:8080" body = base64.b64encode(json.dumps({"amount": 42}).encode()).decode() # Publish immediately requests.post(f"{BASE}/namespaces/payments/queues/invoices/messages", json={"body": body}) # Schedule in 1 hour deliver_at = int(time.time() * 1000) + 3_600_000 requests.post(f"{BASE}/namespaces/payments/queues/invoices/messages", json={"body": body, "deliver_at": deliver_at}) # Consume r = requests.get(f"{BASE}/namespaces/payments/queues/invoices/messages?n=10") for msg in r.json()["messages"]: process(base64.b64decode(msg["body"])) requests.delete(f"{BASE}/messages/{msg['receipt_handle']}")
Node.js (fetch) Node
const BASE = "http://localhost:8080"; const body = Buffer.from(JSON.stringify({ amount: 42 })).toString("base64"); // Publish immediately await fetch(`${BASE}/namespaces/payments/queues/invoices/messages`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ body }), }); // Schedule in 1 hour await fetch(`${BASE}/namespaces/payments/queues/invoices/messages`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ body, deliver_at: Date.now() + 3_600_000 }), }); // Consume + ACK const { messages } = await fetch( `${BASE}/namespaces/payments/queues/invoices/messages?n=10` ).then(r => r.json()); for (const msg of messages) { process(Buffer.from(msg.body, "base64")); await fetch(`${BASE}/messages/${msg.receipt_handle}`, { method: "DELETE" }); }

See it live

Queue depth updating in real time.
Scheduled message firing on cue.

Ready count ticks up as messages are published. A scheduled message sits in "Scheduled" until its deliver_at fires — then it flips to "Ready" automatically.

PulseMQ live demo — queue depth updating and scheduled delivery firing

Quick start

Running in under 60 seconds.

Docker Compose is the fastest path. A single binary works just as well.

1

Clone & start

git clone https://github.com/sneh-joshi/pulsemq cd pulsemq/docker docker compose up -d
2

Open the dashboard

open http://localhost:8080/dashboard
3

Publish your first scheduled message

curl -X POST \\ http://localhost:8080/namespaces/demo/queues/jobs/messages \\ -H "Content-Type: application/json" \\ -d '{"body":"aGVsbG8gd29ybGQ=", "deliver_at":$(( $(date +%s%3N)+60000 ))}'

↑ Delivers in 60 seconds from now.

4

Add the Go SDK

go get github.com/sneh-joshi/pulsemq/pkg/client
docker-compose.yml
services: pulsemq: image: pulsemq/pulsemq:latest ports: - "8080:8080" # API + dashboard - "9090:9090" # Prometheus metrics volumes: - ./data:/data - ./config.yaml:/config.yaml environment: - PULSEMQ_AUTH_API_KEY=your-secret
Or build from source
git clone https://github.com/sneh-joshi/pulsemq cd pulsemq go build -o pulsemq ./cmd/server ./pulsemq --config config.yaml

Use cases

One queue. Dozens of cron job patterns eliminated.

Any task that runs "later" is a candidate.

📧

Onboarding email sequence

Publish welcome email jobs with deliver_at spaced 1 day, 3 days, 7 days after signup. No cron, no DB polling.

WithDelay(24 * time.Hour)
💳

Payment retry

After a failed charge, schedule a retry in 24 hours. If that fails, schedule another at 72 hours. No scheduler service needed.

WithDelay(24 * time.Hour)
🛒

Abandoned cart recovery

At checkout start, publish a reminder message for 30 minutes later. Cancel it if the order completes — or let it fire and send the nudge.

WithDelay(30 * time.Minute)
📝

Scheduled content publishing

Publish a "go live" job for a blog post at exactly 9 AM Monday. No CMS flag polling, no cron expression debugging.

WithDeliverAt(monday9am)
🔄

Subscription renewal

When a subscription is created, publish a renewal job for 3 days before expiry. When the job fires, publish the next one.

WithDeliverAt(expiryDate - 3 days)
📬

Rate-limited bulk email

Space out 10,000 marketing emails by staggering deliver_at timestamps. No external throttle service needed.

deliver_at: now + i * 100ms

AI & Agents

Built for AI agent pipelines.

Agents need to act later, not just now. deliver_at + DLQ handles scheduling, rate limits, and async jobs without adding another dependency.

🤖

Agent task scheduling

"Remind me to follow up in 3 days" → agent publishes with deliver_at = now + 3 days. Fires exactly when due, no polling loop.

deliver_at: now + 3 days

LLM rate limit handling

Hit a 429? Publish the request with deliver_at = now + retry_after_ms. Consumer picks it up exactly when the window resets. No sleep loops.

deliver_at: now + retry_after_ms

Async AI pipelines

User submits → publish job → return ID immediately. Worker calls LLM, stores result. If it crashes mid-task, visibility timeout requeues automatically.

publish → consume → notify
🧑‍💻

Human-in-the-loop

Agent proposes action → publish to pending_approval. Human approves → ACK fires next step. No response → DLQ triggers auto-decline path.

DLQ → auto-decline
📊

Scheduled AI reports

Publish "generate weekly report" with deliver_at = next Monday 9am. Worker calls LLM, emails result. No cron job, no always-on process.

deliver_at: monday9am
🔗

Multi-step agent chains

Step 1 completes → publishes step 2 with a delay. Each step is durable, retriable, and observable from the dashboard.

step1 → delay → step2
agent.py — schedule a follow-up task
import time, base64, requests BASE = "http://localhost:8080" # Agent schedules a follow-up 3 days from now deliver_at = int(time.time() * 1000) + (3 * 24 * 3600 * 1000) requests.post( f"{BASE}/namespaces/agents/queues/followups/messages", json={ "body": base64.b64encode(b"email_followup:lead_id:123").decode(), "deliver_at": deliver_at, } ) # Message fires in exactly 3 days. No cron. No polling. No extra service.

Performance

Built for production.

Single-node numbers. Throughput varies significantly by environment — these are estimated ranges.

Metric Linux VPS
(2–4 vCPU, shared SSD)
Docker Desktop
(macOS / Windows VM)
Notes
Write throughput (fsync=interval) ~15,000 msgs/sec ~4,000 msgs/sec Default config. Bottleneck: WAL fsync + disk I/O.
Write throughput (fsync=never) ~80,000 msgs/sec ~4,000 msgs/sec Lossy mode — no durability guarantee on crash.
Read throughput ~30,000 msgs/sec ~2,000 msgs/sec 50 concurrent consumers, batch=100.
Delivery accuracy ±15–30 ms ±50–200 ms VM timer jitter dominates on Docker Desktop.
Scheduled heap size 1M entries ≈ ~100 MB RAM In-memory Min-Heap. Linear with entry count.
Maximum connections ~65,000 TCP OS ulimit — not a PulseMQ limit.
In-memory index @ 10 GB RAM ~100M messages bbolt page cache.
Docker image size ~8 MB Multi-stage scratch build. Measured.
Minimum RAM 512 MB Runs comfortably on free-tier VMs.
Max message size 256 KB Configurable via max_message_size_kb. Matches SQS's limit — for larger payloads, store data in object storage and pass a reference ID in the message body.
Max messages per queue 100,000 Default cap. Configurable via max_messages per queue.
Max batch size (consume) 100 msgs Max messages returned per Consume call. Configurable via max_batch_size.
Message metadata ≤16 keys · key ≤64 B · value ≤512 B String key/value pairs attached at publish time. Use for routing hints, trace IDs, or tags. Enforced at publish — rejected with 400 if exceeded.

* All throughput numbers are estimated. Write numbers: 50 concurrent producers, batch size 100, measured over 30 seconds. Read numbers: 50 concurrent consumers, batch size 100. Docker Desktop numbers measured on macOS with Apple Silicon.


Open source

MIT licensed. Community driven.

PulseMQ is fully open source. Contributions, bug reports, and feature discussions are welcome.

Star on GitHub Contributing Guide Documentation
Go 1.24 MIT License