At-least-once delivery
A received message is hidden while work is in progress. If the worker disappears, it becomes available again.
Durable queues and Pub/Sub, hosted for your applications. Bring people and agents into one organization, give each team its own workspace, and let PlainQ run the infrastructure.
A visitor carrying a letter joins the line. Each visitor represents one queued message.
Keep slow work from making people wait.
Unfinished jobs stay ready for another try.
Add help when demand climbs.
Know what is waiting, working, and done.
PlainQ keeps unfinished work available without forcing your application to coordinate every worker. The delivery model is small enough to explain—and complete enough to trust.
A received message is hidden while work is in progress. If the worker disappears, it becomes available again.
Set visibility, retention, and receive limits per queue. Drop exhausted work or send it to a dead-letter queue.
Send many messages in one call and receive batches of up to ten when workers are ready for more.
PlainQ carries bytes as-is: JSON, protobuf, text, or a pointer to a larger object.
A worker can receive it now.
Hidden from other workers for 30 seconds.
The message is removed.
Attempt 2 becomes ready.
Keep the same durable queue semantics whether one worker group handles a job or several independent consumers need the same event.
stableAdd consumers without coordinating them. Each message goes to one worker and returns to the queue if it is not acknowledged.
Three clerks share one waiting line. There is still only one visitor carrying one message.
POST /queue/{queueId}/messages · Bearer {teamApiKey}stable v1Every subscribed queue gets its own durable copy, with its own retries, retention, and dead-letter policy.
Publish order.created once. Three distinct visitors collect matching copies of the same letter.
POST /queue/topics/{topicId}/publish · Bearer {teamApiKey}Use PlainQ from code, a browser, or an agent. Bring people into an organization, keep work inside teams, and connect through the same small queue model.
Send, receive, and acknowledge messages through a simple JSON API. Connect with the tools your application already uses.
Create queues, browse messages, and connect Pub/Sub topics in a familiar PlainQ dashboard, organized around your teams.
Bring your people together in an organization. Choose one plan and share its resource limits across your teams.
Give each team its own queues and topics. Assign organization members to the teams they work with.
Issue API keys for a specific team. Applications can use that team's resources without reaching another team's queues.
Authorization: Bearer {teamApiKey}PlainQ prepares the queue infrastructure behind your organization based on its plan. Your team works with queues and topics.
your plan → your workspaceEngineering has its own queues, topics and API keys. The organization shares one plan.
Create an organization, choose a plan, and add your teams. Each team gets its own queues, topics, and API keys. PlainQ prepares the infrastructure behind your workspace.
Create your organizationCreate a queue and send bytes with the official Go SDK. SetPLAINQ_ADDR to your native PlainQ gRPC address. Cloud team endpoints currently use HTTP; this SDK example connects directly to a PlainQ server over gRPC.
go get github.com/marsolab/plainq-go-sdkGo SDK documentation →package main
import (
"context"
"log"
"os"
"time"
plainq "github.com/marsolab/plainq-go-sdk"
v1 "github.com/marsolab/plainq-go-sdk/v1"
)
func main() {
client, err := plainq.New(os.Getenv("PLAINQ_ADDR"))
if err != nil { log.Fatal(err) }
defer client.Close()
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
queue, err := client.CreateQueue(ctx, &v1.CreateQueueRequest{
QueueName: "jobs",
})
if err != nil { log.Fatal(err) }
sent, err := client.Send(ctx, &v1.SendRequest{
QueueId: queue.GetQueueId(),
Messages: []*v1.SendMessage{
{Body: []byte("hello, plainq")},
},
})
if err != nil { log.Fatal(err) }
if len(sent.GetMessageIds()) != 1 { log.Fatal("message was not enqueued") }
log.Printf("queue ID: %s", queue.GetQueueId())
}Keep slow, bursty, and failure-prone work outside the request path. Use familiar queue names instead of building a messaging platform first.
queue / media-jobsBuffer uploads, add workers when demand rises, and retry a conversion if a worker exits halfway through.
queue / webhook-inboxAccept an event quickly, process it off the request path, and keep transient downstream failures recoverable.
queue / order-eventsGive notifications, analytics, and audit processing their own queues and retry policies behind one topic.
queue / agent-runsQueue long-running tasks and inspect their progress through the same team-scoped JSON API your application uses.
Queues, Pub/Sub, and team workspaces in every plan. Choose the room your organization needs, and let PlainQ handle the infrastructure.
For your first application.
$19/ month
Per organization · USD
For teams building together.
$49/ month
Per organization · USD
For more teams and workloads.
$149/ month
Per organization · USD
Create your organization, connect your application, and put your queues to work. PlainQ takes care of the infrastructure underneath.
Go SDK · gRPC: receive and acknowledge a message from a native PlainQ server. Use the same PLAINQ_ADDR as the send example and set PLAINQ_QUEUE_ID to its printed queue ID. For Cloud HTTP connections, follow the Cloud quickstart.
package main
import (
"context"
"log"
"os"
"time"
plainq "github.com/marsolab/plainq-go-sdk"
v1 "github.com/marsolab/plainq-go-sdk/v1"
)
func main() {
client, err := plainq.New(os.Getenv("PLAINQ_ADDR"))
if err != nil { log.Fatal(err) }
defer client.Close()
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
queueID := os.Getenv("PLAINQ_QUEUE_ID")
received, err := client.Receive(ctx, &v1.ReceiveRequest{
QueueId: queueID, BatchSize: 1, WaitTimeSeconds: 5,
})
if err != nil { log.Fatal(err) }
for _, message := range received.GetMessages() {
log.Printf("received: %s", message.GetBody())
ack, err := client.Delete(ctx, &v1.DeleteRequest{
QueueId: queueID, MessageIds: []string{message.GetId()},
})
if err != nil { log.Fatal(err) }
if len(ack.GetFailed()) > 0 { log.Fatal(ack.GetFailed()) }
}
}