diff --git a/src/data/nav/chat.ts b/src/data/nav/chat.ts
index af612eee13..b8e48f3013 100644
--- a/src/data/nav/chat.ts
+++ b/src/data/nav/chat.ts
@@ -223,6 +223,10 @@ export default {
},
],
},
+ {
+ name: 'Chat pricing',
+ link: '/docs/chat/pricing',
+ },
{
name: 'Guides',
pages: [
diff --git a/src/data/nav/liveobjects.ts b/src/data/nav/liveobjects.ts
index 11e38a69e2..215ea8497d 100644
--- a/src/data/nav/liveobjects.ts
+++ b/src/data/nav/liveobjects.ts
@@ -55,11 +55,6 @@ export default {
link: '/docs/liveobjects/concepts/synchronization',
languages: ['javascript', 'swift', 'java'],
},
- {
- name: 'Billing',
- link: '/docs/liveobjects/concepts/billing',
- languages: ['javascript', 'swift', 'java'],
- },
],
},
{
@@ -104,6 +99,10 @@ export default {
},
],
},
+ {
+ name: 'LiveObjects pricing',
+ link: '/docs/liveobjects/pricing',
+ },
],
api: [
{
diff --git a/src/data/nav/platform.ts b/src/data/nav/platform.ts
index 8e1df1417d..a5070715b1 100644
--- a/src/data/nav/platform.ts
+++ b/src/data/nav/platform.ts
@@ -125,6 +125,10 @@ export default {
link: '/docs/platform/pricing/billing',
name: 'Billing',
},
+ {
+ link: '/docs/platform/pricing/message-counting',
+ name: 'Message counting',
+ },
{
link: '/docs/platform/pricing/limits',
name: 'Limits',
diff --git a/src/data/nav/pubsub.ts b/src/data/nav/pubsub.ts
index 6936a8f9ab..ecf3d63a1f 100644
--- a/src/data/nav/pubsub.ts
+++ b/src/data/nav/pubsub.ts
@@ -356,6 +356,10 @@ export default {
},
],
},
+ {
+ name: 'Pub/Sub pricing',
+ link: '/docs/pub-sub/pricing',
+ },
{
name: 'Guides',
pages: [
diff --git a/src/data/nav/spaces.ts b/src/data/nav/spaces.ts
index e230c99349..811b2054fb 100644
--- a/src/data/nav/spaces.ts
+++ b/src/data/nav/spaces.ts
@@ -57,6 +57,10 @@ export default {
},
],
},
+ {
+ name: 'Spaces pricing',
+ link: '/docs/spaces/pricing',
+ },
],
api: [
{
diff --git a/src/pages/docs/chat/pricing.mdx b/src/pages/docs/chat/pricing.mdx
new file mode 100644
index 0000000000..3d9c3af250
--- /dev/null
+++ b/src/pages/docs/chat/pricing.mdx
@@ -0,0 +1,69 @@
+---
+title: "Chat pricing"
+meta_description: "Understand how Chat SDK features contribute to your message count, including messages, typing indicators, reactions, and cost optimization strategies."
+meta_keywords: "chat pricing, message counting, typing indicators, reactions, cost optimization"
+intro: "How Chat SDK features contribute to your message count and strategies to optimize costs."
+redirect_from:
+ - /docs/chat/message-billing
+---
+
+The [Chat SDK](/docs/chat) is built on top of [Pub/Sub](/docs/pub-sub). All Chat operations generate Pub/Sub messages that follow the same counting rules.
+
+
+
+## Chat operations
+
+The following table shows how Chat operations contribute to your message count:
+
+| Operation | Messages counted |
+| --- | --- |
+| [Send message](/docs/chat/rooms/messages) | 1 inbound message |
+| Message delivery | 1 outbound message per subscriber |
+| [Update message](/docs/chat/rooms/messages#update) | 1 inbound message |
+| Message update delivery | 1 outbound message per subscriber |
+| [Delete message](/docs/chat/rooms/messages#delete) | 1 inbound message |
+| Message deletion delivery | 1 outbound message per subscriber |
+| [History](/docs/chat/rooms/history) retrieval | 1 message per retrieved message |
+| [Typing indicator](/docs/chat/rooms/typing) keystroke | 1 inbound message; repeats every heartbeat interval (default 10s) |
+| Typing indicator delivery | 1 outbound message per subscriber per heartbeat |
+| [Typing indicator](/docs/chat/rooms/typing) stop | 1 inbound message |
+| Typing stop delivery | 1 outbound message per subscriber |
+| [Room reaction](/docs/chat/rooms/reactions) | 1 inbound message |
+| Room reaction delivery | 1 outbound message per subscriber |
+| [Message reaction](/docs/chat/rooms/message-reactions) send | 1 inbound message |
+| [Message reaction](/docs/chat/rooms/message-reactions) delete | 1 inbound message |
+| Message reaction summary delivery | 1 outbound message per subscriber; multiple reactions may be rolled up into a single summary |
+| [Presence enter](/docs/chat/rooms/presence) | 1 inbound message |
+| [Presence leave](/docs/chat/rooms/presence) | 1 inbound message |
+| [Presence update](/docs/chat/rooms/presence) | 1 inbound message |
+| Presence event delivery | 1 outbound message per presence subscriber |
+| [Occupancy](/docs/chat/rooms/occupancy) event | 1 outbound message per subscriber (generated on membership changes, debounced up to 15s) |
+| [Moderation](/docs/chat/moderation) action | 1 inbound message; triggers a message update or delete which follows standard delivery |
+
+## Rooms, channels, and connections
+
+Each Chat room maps to underlying Pub/Sub channels. Each room feature (messages, typing, reactions, presence, occupancy) uses its own channel, contributing to your [channel count](/docs/platform/pricing#channels). Ably bills each connected client for [connection minutes](/docs/platform/pricing#connections) in the same way as Pub/Sub.
+
+## Cost optimization
+
+### Reduce typing indicator frequency
+
+Increase the `heartbeatThrottleMs` [room option](/docs/chat/rooms#typing) to reduce typing indicator event frequency. The default is 10 seconds. Increase this value in rooms that tolerate delayed typing feedback.
+
+### Disable typing indicators
+
+Disable typing indicators entirely in rooms where they are not needed. This eliminates a significant source of messages, especially in rooms with many participants.
+
+### Use server-side batching
+
+[Server-side batching](/docs/guides/pub-sub/data-streaming#solution-server-side-batching) groups messages into single deliveries. Use this for high-throughput rooms where slight delay is acceptable.
+
+### Use occupancy instead of presence
+
+Use [occupancy](/docs/chat/rooms/occupancy) instead of [presence](/docs/chat/rooms/presence) when you only need member counts, not individual identities. This avoids the n-squared presence event fan-out. For example, 200 members joining and leaving generates approximately **80,400 messages**. See [large-scale presence sets](/docs/presence-occupancy/presence#large-presence) for details.
+
+### Enable server-side presence batching
+
+Enable [server-side batching](/docs/presence-occupancy/presence#large-presence) to group presence events. Subscribe to presence updates only on rooms where you need member-level detail.
diff --git a/src/pages/docs/liveobjects/index.mdx b/src/pages/docs/liveobjects/index.mdx
index 999df0e6aa..929e3aef74 100644
--- a/src/pages/docs/liveobjects/index.mdx
+++ b/src/pages/docs/liveobjects/index.mdx
@@ -96,7 +96,7 @@ LiveObjects [durably stores](/docs/liveobjects/storage) all objects on a channel
LiveObjects usage is billed based on [Ably's standard pricing model](/docs/platform/pricing). Since LiveObjects is powered by Ably's channel messages, any interaction with LiveObjects - whether sending or receiving operations, maintaining active channels, or keeping connections open through the Realtime LiveObjects API - translates into billable usage. Storage of objects themselves does not incur additional costs; however, there is a [limit](/docs/liveobjects/storage) on the size of the channel object.
-For details on how using LiveObjects contributes to your billable usage, see [Billing](/docs/liveobjects/concepts/billing).
+For details on how using LiveObjects contributes to your billable usage, see [LiveObjects pricing](/docs/liveobjects/pricing).
### Realtime API
diff --git a/src/pages/docs/liveobjects/concepts/billing.mdx b/src/pages/docs/liveobjects/pricing.mdx
similarity index 68%
rename from src/pages/docs/liveobjects/concepts/billing.mdx
rename to src/pages/docs/liveobjects/pricing.mdx
index a4d0420107..e8e7f9aea3 100644
--- a/src/pages/docs/liveobjects/concepts/billing.mdx
+++ b/src/pages/docs/liveobjects/pricing.mdx
@@ -1,11 +1,15 @@
---
-title: Billing
-meta_description: "Understand how LiveObjects operations contribute to your Ably usage and billing."
+title: "LiveObjects pricing"
+meta_description: "Understand how LiveObjects operations contribute to your message count, including LiveMap, LiveCounter, synchronization, and REST API usage."
+meta_keywords: "LiveObjects pricing, message counting, LiveMap, LiveCounter, ObjectMessages"
+intro: "How LiveObjects operations contribute to your message count."
+redirect_from:
+ - /docs/liveobjects/concepts/billing
---
@@ -25,26 +29,38 @@ meta_description: "Understand how LiveObjects operations contribute to your Ably
-LiveObjects operations are billed as messages. This page explains how different LiveObjects operations contribute to your Ably usage.
+LiveObjects operations are billed using ObjectMessages. Each ObjectMessage follows the standard inbound/outbound counting pattern.
-## Message counting
+## LiveObjects operations
-Each operation is sent as an [`ObjectMessage`](/docs/liveobjects/concepts/operations#properties). Ably bills for each outbound message sent from a client, and for each inbound message delivered to each client.
+The following table shows how LiveObjects operations contribute to your message count:
-When a client performs an operation (such as setting a value on a `LiveMap` or incrementing a `LiveCounter`), this generates an outbound message. When that operation is broadcast to other connected clients, each client receives an inbound message.
+| Operation | Messages counted |
+| --- | --- |
+| [LiveMap](#livemap-operations) set or remove | 1 inbound message per operation |
+| [LiveMap](#livemap-operations) create (shallow) | 2 inbound messages (create + assign) |
+| [LiveCounter](#livecounter-operations) increment or decrement | 1 inbound message |
+| [LiveCounter](#livecounter-operations) create | 2 inbound messages (create + assign) |
+| ObjectMessage delivery | 1 outbound message per connected client |
+| [Synchronization](#synchronization) | 1 message per object synchronized |
+| [REST API](#rest-api) fetch | 1 message per object in response |
+| [Batch](#livemap-operations) operation | 1 message per operation in the batch |
-For example, if 5 clients are connected to a channel and one client increments a counter:
+Nested object creation generates additional messages for each nested object. See the [examples below](#livemap-operations) for details.
-- 1 outbound message (from the client performing the increment)
-- 5 inbound messages (one delivered to each of the 5 connected clients, including the client that sent it)
+## Channels and connections
+
+Each LiveObjects channel contributes to your [channel count](/docs/platform/pricing#channels). Ably bills [connection minutes](/docs/platform/pricing#connections) per minute of connection time for each connected client.
+
+Subscribing to updates does not affect the number of messages received by a client. Any client attached to a channel with the `object-subscribe` capability automatically receives all object messages for that channel. Subscribing to updates on an object adds a listener that is called whenever the client receives updates for that object.
-## LiveMap operations
+## LiveMap operation examples
Removing a key and setting a primitive value always sends one message:
@@ -100,7 +116,7 @@ await myObject.get('settings').batch((ctx) => {
```
-## LiveCounter operations
+## LiveCounter operation examples
Incrementing and decrementing a counter always sends one message:
@@ -165,10 +181,6 @@ Similarly, if a client becomes disconnected and needs to resynchronize, it will
Only [reachable](/docs/liveobjects/concepts/objects#reachability) objects are counted. Ably may send [tombstone](/docs/liveobjects/concepts/objects#tombstones) objects to the client, but these will not count towards your usage.
-## Subscriptions
-
-Subscribing to updates does not affect the number of messages received by a client. Any client attached to a channel with the `object-subscribe` capability automatically receives all object messages for that channel. Subscribing to updates on an object adds a listener that is called whenever the client receives updates for that object.
-
## REST API
The [LiveObjects REST API](/docs/liveobjects/rest-api-usage) also counts messages for operations performed.
diff --git a/src/pages/docs/platform/pricing/message-counting.mdx b/src/pages/docs/platform/pricing/message-counting.mdx
new file mode 100644
index 0000000000..86b1642648
--- /dev/null
+++ b/src/pages/docs/platform/pricing/message-counting.mdx
@@ -0,0 +1,156 @@
+---
+title: "Message counting"
+meta_description: "Understand which operations across all Ably products count as messages, including Pub/Sub, Chat, LiveObjects, Spaces, AI Transport, and LiveSync."
+meta_keywords: "message counting, pricing, Pub/Sub messages, Chat messages, LiveObjects messages, Spaces messages, AI Transport messages, LiveSync messages"
+intro: "Messages are the primary unit of consumption that Ably charges for. Understanding which operations count as messages helps you estimate and manage costs."
+---
+
+
+
+## How Ably counts messages
+
+As a general rule, most operations involve publishing and receiving messages. Each operation generates:
+
+* 1 inbound message when a client publishes or performs an operation
+* 1 outbound message for each subscriber that receives it
+
+For example, if 1 client publishes a message to a channel with 10 subscribers, this counts as **11 messages** (1 inbound + 10 outbound).
+
+By default, [echo messages](/docs/pub-sub/advanced#echo) are enabled, which means the publisher also receives its own message as an outbound delivery. In the example above, echo adds 1 outbound message for a total of **12 messages**.
+
+Client-side [message filtering](/docs/api/realtime-sdk/channels#subscribe) does not reduce your message count. Ably counts all messages delivered to a connection, regardless of whether the client filters them.
+
+The tables below list all operations that count as messages. Unless noted otherwise, each operation follows this inbound/outbound pattern. `Time` and `Ping` operations are not counted.
+
+## Message size
+
+Ably calculates message size as the sum of the `name`, `clientId`, `data`, and `extras` [properties](/docs/api/realtime-sdk/messages#properties) before any compression or expansion occurs in the serialization process. This applies to all products built on Pub/Sub, including Chat.
+
+* `name` and `clientId` are calculated as the size in bytes of their UTF-8 representation.
+* `data` is calculated as the size in bytes if it is binary, or its UTF-8 byte length if it is a string.
+* `extras` is calculated as the string length of its JSON representation.
+
+Ably bills messages in 5KiB chunks. A message up to 5KiB counts as 1 message. A 50KiB message counts as 10 messages, and a 16KiB message counts as 4 messages.
+
+Each package has a bandwidth allowance based on an average message size of 5KiB. If your total bandwidth for the month exceeds this baseline, Ably charges overage per GiB. See [maximum message size](/docs/platform/pricing/limits#message) for the size limits per package type.
+
+## Pub/Sub operations
+
+The following table shows how [Pub/Sub](/docs/pub-sub) operations contribute to your message count:
+
+| Operation | Messages counted |
+| --- | --- |
+| [Publish](/docs/channels/messages#publish) | 1 inbound message |
+| Message delivery | 1 outbound message per subscriber |
+| [Presence enter](/docs/presence-occupancy/presence) | 1 inbound message |
+| [Presence leave](/docs/presence-occupancy/presence) | 1 inbound message |
+| [Presence update](/docs/presence-occupancy/presence) | 1 inbound message |
+| Presence event delivery | 1 outbound message per presence subscriber |
+| [Persistence](/docs/storage-history/storage) storage | 1 additional message per stored message |
+| [History](/docs/storage-history/history) retrieval | 1 message per retrieved message |
+| [Integration](/docs/platform/integrations) delivery | 1 outbound message per integration target |
+| [Rewind](/docs/channels/options/rewind) on attach | 1 message per rewound message (up to 100) |
+| [Push notification](/docs/push) delivery | 1 message per delivered notification |
+| [Presence REST](/docs/presence-occupancy/presence) query | 1 message per member returned |
+| [Batch presence](/docs/presence-occupancy/presence) request | 1 message per member across all queried channels |
+| [Annotation](/docs/messages/annotations) publish | 1 inbound message |
+| [Annotation](/docs/messages/annotations) delete | 1 inbound message |
+| [Annotation summary](/docs/messages/annotations#annotation-summaries) delivery | 1 outbound message per subscriber; multiple annotations may be rolled up into a single summary |
+| [Lifecycle event](/docs/metadata-stats/metadata) (`[meta]connection.lifecycle`, `[meta]channel.lifecycle`) | 1 message per event |
+| [`[meta]stats:minute`](/docs/metadata-stats/metadata) event | 1 message per event |
+| [`[meta]log`](/docs/metadata-stats/metadata) subscription | Not counted |
+
+For Pub/Sub-specific cost optimization strategies, see [Pub/Sub pricing](/docs/pub-sub/pricing).
+
+## Chat operations
+
+The [Chat SDK](/docs/chat) is built on top of [Pub/Sub](/docs/pub-sub). All Chat operations generate Pub/Sub messages that follow the same counting rules.
+
+| Operation | Messages counted |
+| --- | --- |
+| [Send message](/docs/chat/rooms/messages) | 1 inbound message |
+| Message delivery | 1 outbound message per subscriber |
+| [Update message](/docs/chat/rooms/messages#update) | 1 inbound message |
+| Message update delivery | 1 outbound message per subscriber |
+| [Delete message](/docs/chat/rooms/messages#delete) | 1 inbound message |
+| Message deletion delivery | 1 outbound message per subscriber |
+| [History](/docs/chat/rooms/history) retrieval | 1 message per retrieved message |
+| [Typing indicator](/docs/chat/rooms/typing) keystroke | 1 inbound message; repeats every heartbeat interval (default 10s) |
+| Typing indicator delivery | 1 outbound message per subscriber per heartbeat |
+| [Typing indicator](/docs/chat/rooms/typing) stop | 1 inbound message |
+| Typing stop delivery | 1 outbound message per subscriber |
+| [Room reaction](/docs/chat/rooms/reactions) | 1 inbound message |
+| Room reaction delivery | 1 outbound message per subscriber |
+| [Message reaction](/docs/chat/rooms/message-reactions) send | 1 inbound message |
+| [Message reaction](/docs/chat/rooms/message-reactions) delete | 1 inbound message |
+| Message reaction summary delivery | 1 outbound message per subscriber; multiple reactions may be rolled up into a single summary |
+| [Presence enter](/docs/chat/rooms/presence) | 1 inbound message |
+| [Presence leave](/docs/chat/rooms/presence) | 1 inbound message |
+| [Presence update](/docs/chat/rooms/presence) | 1 inbound message |
+| Presence event delivery | 1 outbound message per presence subscriber |
+| [Occupancy](/docs/chat/rooms/occupancy) event | 1 outbound message per subscriber (generated on membership changes, debounced up to 15s) |
+| [Moderation](/docs/chat/moderation) action | 1 inbound message; triggers a message update or delete which follows standard delivery |
+
+For Chat-specific cost optimization strategies, see [Chat pricing](/docs/chat/pricing).
+
+## LiveObjects operations
+
+[LiveObjects](/docs/liveobjects) operations are billed using ObjectMessages. Each ObjectMessage follows the standard inbound/outbound counting pattern.
+
+| Operation | Messages counted |
+| --- | --- |
+| [LiveMap](/docs/liveobjects/pricing#livemap-operations) set or remove | 1 inbound message per operation |
+| [LiveMap](/docs/liveobjects/pricing#livemap-operations) create (shallow) | 2 inbound messages (create + assign) |
+| [LiveCounter](/docs/liveobjects/pricing#livecounter-operations) increment or decrement | 1 inbound message |
+| [LiveCounter](/docs/liveobjects/pricing#livecounter-operations) create | 2 inbound messages (create + assign) |
+| ObjectMessage delivery | 1 outbound message per connected client |
+| [Synchronization](/docs/liveobjects/pricing#synchronization) | 1 message per object synchronized |
+| [REST API](/docs/liveobjects/pricing#rest-api) fetch | 1 message per object in response |
+| [Batch](/docs/liveobjects/pricing#livemap-operations) operation | 1 message per operation in the batch |
+
+Nested object creation generates additional messages for each nested object. See the [LiveObjects pricing](/docs/liveobjects/pricing) page for detailed examples.
+
+## Spaces operations
+
+The [Spaces SDK](/docs/spaces) is built on top of [Pub/Sub](/docs/pub-sub) channels and [presence](/docs/presence-occupancy/presence). All Spaces operations generate Pub/Sub messages that follow the same counting rules.
+
+| Operation | Messages counted |
+| --- | --- |
+| [Enter](/docs/spaces/space#enter) space | 1 inbound message |
+| [Leave](/docs/spaces/space#leave) space | 1 inbound message |
+| [Update profile](/docs/spaces/space#update-profile) | 1 inbound message |
+| Space event delivery | 1 outbound message per subscriber |
+| [Set location](/docs/spaces/locations#set) | 1 inbound message |
+| Location event delivery | 1 outbound message per subscriber |
+| [Set cursor position](/docs/spaces/cursors#set) | 1 inbound message per batch (default batch interval 25ms) |
+| Cursor event delivery | 1 outbound message per subscriber |
+| [Lock acquire](/docs/spaces/locking#acquire) | 1 inbound message |
+| [Lock release](/docs/spaces/locking#release) | 1 inbound message |
+| Lock event delivery | 1 outbound message per subscriber |
+
+Live cursors use a [separate channel](/docs/spaces/cursors#foundations) from other space features due to their high update frequency. Registering multiple subscription listeners for the same event does not increase your message count, as these are [client-side filtered events](/docs/spaces/space#subscribe).
+
+## AI Transport operations
+
+[AI Transport](/docs/ai-transport) uses standard Pub/Sub messaging. Each token or message published to Ably counts as an inbound message, and each delivery to a subscriber counts as an outbound message.
+
+| Operation | Messages counted |
+| --- | --- |
+| Token publish | 1 inbound message per published message |
+| Token delivery | 1 outbound message per subscriber |
+| [Persistence](/docs/storage-history/storage) storage | 1 additional message per stored message |
+
+The total cost depends on the [token streaming pattern](/docs/ai-transport/token-streaming#token-streaming-patterns) you choose. With [message-per-response](/docs/ai-transport/token-streaming/message-per-response), [append rollup](/docs/ai-transport/token-streaming/token-rate-limits#per-response) conflates multiple tokens into fewer inbound messages. For example, 300 tokens published individually can be conflated to approximately 100 inbound messages. See the [AI support chatbot pricing example](/docs/platform/pricing/examples/ai-chatbot) for a full breakdown.
+
+## LiveSync operations
+
+[LiveSync](/docs/livesync) publishes database changes as Pub/Sub messages. Each update follows the standard inbound/outbound counting pattern.
+
+| Operation | Messages counted |
+| --- | --- |
+| Database update publish | 1 inbound message |
+| Update delivery | 1 outbound message per subscriber |
+
+For example, if the database connector publishes 1 update and 3 clients are subscribed, this counts as **4 messages** (1 inbound + 3 outbound).
diff --git a/src/pages/docs/pub-sub/pricing.mdx b/src/pages/docs/pub-sub/pricing.mdx
new file mode 100644
index 0000000000..27282326ef
--- /dev/null
+++ b/src/pages/docs/pub-sub/pricing.mdx
@@ -0,0 +1,74 @@
+---
+title: "Pub/Sub pricing"
+meta_description: "Understand how Pub/Sub operations contribute to your message count, including persistence, presence, and cost optimization strategies."
+meta_keywords: "message counting, pricing, persistence, presence, cost optimization, Pub/Sub pricing"
+intro: "How Pub/Sub operations contribute to your message count and strategies to optimize costs."
+redirect_from:
+ - /docs/pub-sub/message-billing
+---
+
+Pub/Sub operations contribute to your message count based on publishing, delivery, and optional features like persistence and presence.
+
+
+
+## Pub/Sub operations
+
+The following table shows how Pub/Sub operations contribute to your message count:
+
+| Operation | Messages counted |
+| --- | --- |
+| [Publish](/docs/channels/messages#publish) | 1 inbound message |
+| Message delivery | 1 outbound message per subscriber |
+| [Presence enter](/docs/presence-occupancy/presence) | 1 inbound message |
+| [Presence leave](/docs/presence-occupancy/presence) | 1 inbound message |
+| [Presence update](/docs/presence-occupancy/presence) | 1 inbound message |
+| Presence event delivery | 1 outbound message per presence subscriber |
+| [Persistence](/docs/storage-history/storage) storage | 1 additional message per stored message |
+| [History](/docs/storage-history/history) retrieval | 1 message per retrieved message |
+| [Integration](/docs/platform/integrations) delivery | 1 outbound message per integration target |
+| [Rewind](/docs/channels/options/rewind) on attach | 1 message per rewound message (up to 100) |
+| [Push notification](/docs/push) delivery | 1 message per delivered notification |
+| [Presence REST](/docs/presence-occupancy/presence) query | 1 message per member returned |
+| [Batch presence](/docs/presence-occupancy/presence) request | 1 message per member across all queried channels |
+| [Annotation](/docs/messages/annotations) publish | 1 inbound message |
+| [Annotation](/docs/messages/annotations) delete | 1 inbound message |
+| [Annotation summary](/docs/messages/annotations#annotation-summaries) delivery | 1 outbound message per subscriber; multiple annotations may be rolled up into a single summary |
+| [Lifecycle event](/docs/metadata-stats/metadata) (`[meta]connection.lifecycle`, `[meta]channel.lifecycle`) | 1 message per event |
+| [`[meta]stats:minute`](/docs/metadata-stats/metadata) event | 1 message per event |
+| [`[meta]log`](/docs/metadata-stats/metadata) subscription | Not counted |
+
+## Channels and connection minutes
+
+[Channels](/docs/channels) are the unit of message distribution. Each channel you use contributes to your [channel count](/docs/platform/pricing#channels). Ably bills [connection minutes](/docs/platform/pricing#connections) per minute of connection time for each connected client.
+
+## Cost optimization
+
+### Use ephemeral messages
+
+Mark messages as [ephemeral](/docs/pub-sub/advanced#ephemeral) to exempt them from persistence, rewind, resume, and integrations. Use this for streaming data where history is not needed.
+
+### Disable self-delivery
+
+Set [echoMessages: false](/docs/pub-sub/advanced#echo) to prevent messages from being delivered back to the publisher. This is useful for optimistic UI patterns and server-side publishers.
+
+### Enable conflation
+
+[Conflation](/docs/guides/pub-sub/data-streaming#solution-message-conflation) delivers only the latest message per key in each time window. Use this for high-frequency updates where only the latest value matters.
+
+### Use server-side batching
+
+[Server-side batching](/docs/guides/pub-sub/data-streaming#solution-server-side-batching) groups messages into single deliveries. Use this for high-throughput channels where slight delay is acceptable.
+
+### Enable delta compression
+
+[Delta compression](/docs/channels/options/deltas) reduces payload size, lowering bandwidth costs. Use this for large, structurally similar successive messages.
+
+### Use occupancy instead of presence
+
+Use [occupancy](/docs/presence-occupancy/occupancy) instead of presence when you only need member counts, not individual identities. This avoids the n-squared presence event fan-out where each member event is delivered to every subscriber. For example, 200 members joining and leaving a channel generates approximately **80,400 messages**. See [large-scale presence sets](/docs/presence-occupancy/presence#large-presence) for the full calculation.
+
+### Enable server-side presence batching
+
+Enable [server-side batching](/docs/presence-occupancy/presence#large-presence) to group presence events and support up to 20,000 members per channel. Subscribe to presence updates only on channels where you need member-level detail.
diff --git a/src/pages/docs/spaces/pricing.mdx b/src/pages/docs/spaces/pricing.mdx
new file mode 100644
index 0000000000..fdabb494fa
--- /dev/null
+++ b/src/pages/docs/spaces/pricing.mdx
@@ -0,0 +1,50 @@
+---
+title: "Spaces pricing"
+meta_description: "Understand how Spaces SDK features contribute to your message count, including avatar stacks, member location, live cursors, and component locking."
+meta_keywords: "Spaces pricing, message counting, live cursors, avatar stacks, component locking"
+intro: "How Spaces SDK features contribute to your message count and strategies to optimize costs."
+---
+
+The [Spaces SDK](/docs/spaces) is built on top of [Pub/Sub](/docs/pub-sub) channels and [presence](/docs/presence-occupancy/presence). All Spaces operations generate Pub/Sub messages that follow the same counting rules.
+
+
+
+## Spaces operations
+
+The following table shows how Spaces operations contribute to your message count:
+
+| Operation | Messages counted |
+| --- | --- |
+| [Enter](/docs/spaces/space#enter) space | 1 inbound message |
+| [Leave](/docs/spaces/space#leave) space | 1 inbound message |
+| [Update profile](/docs/spaces/space#update-profile) | 1 inbound message |
+| Space event delivery | 1 outbound message per subscriber |
+| [Set location](/docs/spaces/locations#set) | 1 inbound message |
+| Location event delivery | 1 outbound message per subscriber |
+| [Set cursor position](/docs/spaces/cursors#set) | 1 inbound message per batch (default batch interval 25ms) |
+| Cursor event delivery | 1 outbound message per subscriber |
+| [Lock acquire](/docs/spaces/locking#acquire) | 1 inbound message |
+| [Lock release](/docs/spaces/locking#release) | 1 inbound message |
+| Lock event delivery | 1 outbound message per subscriber |
+
+Registering multiple subscription listeners for the same event does not increase your message count. These are [client-side filtered events](/docs/spaces/space#subscribe) where only a single message is published per event by Ably.
+
+## Channels and connections
+
+Each space maps to an underlying Pub/Sub [channel](/docs/channels). Live cursors use a [separate channel](/docs/spaces/cursors#foundations) from other space features due to their high update frequency. Both channels contribute to your [channel count](/docs/platform/pricing#channels). Ably bills [connection minutes](/docs/platform/pricing#connections) per minute of connection time for each connected client.
+
+## Cost optimization
+
+### Increase cursor batch interval
+
+Increase the [`outboundBatchInterval`](/docs/spaces/cursors#batch) to reduce the frequency of cursor position updates. The default is 25ms. Increasing this value reduces the number of messages at the cost of less smooth cursor movement.
+
+### Limit concurrent cursor streaming
+
+Ably recommends a maximum of [20 members](/docs/spaces/cursors) simultaneously streaming their cursors in a space for an optimal end-user experience. Beyond this, the volume of cursor messages increases significantly.
+
+### Use occupancy instead of presence for large spaces
+
+For spaces with many members where you only need a count of active users, consider using [occupancy](/docs/presence-occupancy/occupancy) on the underlying channel instead of tracking individual presence events.