Topics and subscriptions
This guide details the Pub/Sub resource model: topic configurations, storage quota mechanics, retention rules, subscription types, and resource modification options.
Environment setup
Set the required environment variables for API requests:
export CAI_PUBSUB_API="https://api.codyhill.net"
export CAI_PROJECT="<your-project-id>"
export CAI_TOKEN="<your-api-key-or-session-token>"
Topics: Named messaging channels
A topic is a named messaging channel where producers publish messages.
Topic settings
| Property | Required | Default | Description |
|---|---|---|---|
name | Yes | — | Immutable. Lowercase letters, digits, and hyphens (1–63 characters). Must start with a letter. |
display_name | No | Empty | Editable human-readable label. |
max_bytes | No | 16Mi | Reclaimed storage budget reserved for this topic's unacknowledged backlog. Editable; lowering it to at or below the live backlog is refused unless you ask for it explicitly. |
max_age | No | None | Maximum retention period for messages (e.g. 24h, 30m). |
discard | No | old | Capacity overflow behavior: old drops the oldest unacknowledged messages; new rejects new publishes with HTTP 409. |
Creating a topic
- platformctl
- curl
- Console UI
platformctl pubsub topics create orders --display-name "Order Events" \
--max-bytes 64Mi --max-age 24h --discard new
curl -sX POST "$CAI_PUBSUB_API/v1/projects/$CAI_PROJECT/topics" \
-H "Authorization: Bearer $CAI_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"orders","display_name":"Order Events","max_bytes":"64Mi","max_age":"24h","discard":"new"}'
- In the Crusoe Console, navigate to Messaging → Pub/Sub.
- Click Create Topic.
- Fill in the display name, storage limit (
64Mi), max message age (24h), and overflow strategy (new). - Click Save.
Storage budget allocation
Each project has a Pub/Sub message storage budget (default 1 GiB).
- Claimed Budget: A topic claims its
max_bytesallocation immediately upon creation, even when empty. - Capacity Management: Deleting unused topics or lowering
max_bytesfrees storage budget for other topics. A topic can only give back budget it is not using — see loweringmax_bytesbelow what the topic is holding. - Monitoring Capacity: Check quota usage anytime via CLI, API, or Console.
- platformctl
- curl
- Console UI
platformctl pubsub quota
curl -s "$CAI_PUBSUB_API/v1/projects/$CAI_PROJECT/pubsub/quota" \
-H "Authorization: Bearer $CAI_TOKEN"
View the Quota Usage bar above the topic table in Messaging → Pub/Sub.
Subscriptions: Durable readers
Subscriptions track reader progress and retain unacknowledged messages for processing.
Subscription configuration
| Property | Required | Default | Description |
|---|---|---|---|
name | Yes | — | Unique identifier within the topic. |
type | No | shared | Ordering and consumer model: shared, key-shared, exclusive, failover. Immutable after creation. |
ack_deadline_seconds | No | 30 | Time window (1–600 seconds) for consumers to acknowledge pulled messages before redelivery. |
max_deliver | No | 5 | Maximum delivery attempts before forwarding to a dead-letter topic (push subscriptions). |
start_from | No | all | Initial read position: all (earliest available) or new (from subscription creation onwards). Immutable. |
dead_letter | No | None | Target topic configuration for unprocessable messages: {"topic": "dead-letter-topic", "after_attempts": 5}. |
Subscription ordering models
shared: Competing consumer model. Messages are distributed across active workers for parallel processing.key-shared: Partitioned queue. Messages sharing the samekeyattribute are delivered in strict order to the same consumer instance.exclusive: Single active consumer stream with strict total ordering.failover: Primary single consumer stream with automatic failover to standby consumers.
Creating a subscription
- platformctl
- curl
- Console UI
platformctl pubsub subscriptions create workers --topic orders \
--type key-shared --ack-deadline-seconds 60 --max-ack-pending 500 --start-from new
curl -sX POST "$CAI_PUBSUB_API/v1/projects/$CAI_PROJECT/topics/orders/subscriptions" \
-H "Authorization: Bearer $CAI_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"workers","type":"key-shared","ack_deadline_seconds":60,"max_ack_pending":500,"start_from":"new"}'
- Open the
orderstopic page. - Click Create Subscription.
- Select
key-sharedtype, enter deadline60seconds, set start point tonew, and click Create.
Updating resource settings
Dynamic topic and subscription settings can be modified in place. Immutable properties (name, type, start_from) require deleting and recreating the resource.
Updating a topic limit
- platformctl
- curl
- Console UI
platformctl pubsub topics update orders --max-bytes 128Mi
curl -sX PATCH "$CAI_PUBSUB_API/v1/projects/$CAI_PROJECT/topics/orders" \
-H "Authorization: Bearer $CAI_TOKEN" \
-H "Content-Type: application/json" \
-d '{"max_bytes":"128Mi"}'
- Select Edit on the topic row.
- Adjust the max bytes value and save changes.
Lowering max_bytes below what the topic is holding
Raising max_bytes is checked against the project's storage budget and nothing else, and restating the same size in different units is not a lowering at all. Lowering it is checked against the topic's live backlog — the unacknowledged bytes it is holding right now — because a ceiling below the backlog cannot be honoured without something giving way, and what gives way is not the same on every topic.
A PATCH that would put max_bytes at or below the current backlog is refused with 409. A topic sitting exactly at its ceiling drops or rejects on its very next message, so an equal pair is refused as well. The refusal names both numbers — what the topic is holding and the ceiling you asked for — and spells out the consequence for that topic's discard setting, because the two settings fail in opposite directions:
discard | What lowering below the backlog would do |
|---|---|
old (the default) | The broker drops the oldest unacknowledged messages until the backlog fits under the new ceiling. Immediate, permanent, and nothing is dead-lettered on the way out. |
new | Nothing is dropped. Publishing starts failing instead, until consumers drain the backlog below the new ceiling. |
Both of those used to happen silently, on a plain edit, at every surface. Neither does now.
Read the backlog before you lower the ceiling. It is the same figure the check reads, so there is no second number to reconcile:
- platformctl
- curl
- Console UI
platformctl pubsub usage
The per-topic table has a BACKLOG column — what the topic is still holding unacknowledged.
curl -s "$CAI_PUBSUB_API/v1/projects/$CAI_PROJECT/topics/orders" \
-H "Authorization: Bearer $CAI_TOKEN" | jq .stats.backlog_bytes
The console's Pub/Sub views carry the same counters, resynced from the broker periodically rather than on every page load. Before you lower a ceiling on the strength of one, read the figure from the API or the CLI instead — that is the reading the check makes.
The override is ?allow_discard=true. Dropping the oldest messages is sometimes exactly what you want — a topic holding a week of events nobody will ever process is claiming budget for nothing. Repeat the call with ?allow_discard=true and the lowering goes through, with the consequence its discard setting implies. This is the same shape as ?cascade=true on project delete: refused by default, explicit when you mean it, never inferred from the plain call.
- platformctl
- curl
- Console UI
platformctl pubsub topics update orders --max-bytes 16Mi --allow-discard
The flag is named for what it costs you, not for the check it lifts, and it is the same word as the ?allow_discard=true it sends — the way --cascade matches ?cascade=true — so there is nothing to translate between the curl above and the command here. On a discard: new topic nothing already stored is discarded; publishes start failing instead, which the command's --help spells out. It only means anything alongside --max-bytes; passing it on its own is an error rather than a no-op.
curl -sX PATCH "$CAI_PUBSUB_API/v1/projects/$CAI_PROJECT/topics/orders?allow_discard=true" \
-H "Authorization: Bearer $CAI_TOKEN" \
-H "Content-Type: application/json" \
-d '{"max_bytes":"16Mi"}'
The console asks before it does it. Lower max_bytes in the edit dialog and the refusal comes back as a confirmation carrying the server's own sentence - the live backlog, the ceiling you asked for, and what this topic's discard policy will do about it. Confirm and the console repeats the call with ?allow_discard=true; cancel and nothing changed. The CLI and the API take the same flag.
On a discard: new topic the flag is still what unlocks the change, and it is worth reading twice there: it discards nothing, it accepts a ceiling your backlog is already above, and publishes fail until consumers bring the backlog back under it.
An acknowledged shrink is recorded as one. The audit row for that edit carries allow_discard: true and the backlog_bytes reading it overrode, so "how much was at stake when someone waved this through" is answerable afterwards. When the topic was not reporting a backlog, the row records null rather than 0 — an override against an unknown backlog is not an override that risked nothing.
If the backlog cannot be read, the lowering is refused. A topic that is not reporting live figures yet has no backlog to compare against — and a backlog_bytes of 0 on a topic that is not ready means unknown, not empty. The refusal names the state it found rather than guessing: an unread number is not a small number, and a guess here is how an edit becomes silent message loss. ?allow_discard=true overrides this case too, which is what keeps a topic whose figures cannot be read from being un-shrinkable forever. Raising max_bytes is never held up by any of it — it cannot discard anything, so it is not checked against the backlog at all.
Summary best practices
- Pre-create Subscriptions: Ensure subscriptions exist prior to publishing messages so no events are missed.
- Configure Dead-Letter Topics: Set up dead-letter targets on push subscriptions to capture and isolate unprocessable messages.
- Select Appropriate Subscription Types: Use
sharedfor parallel task queues andkey-sharedfor entity-ordered stream processing.