# Best Practices for Using the API Efficiently

The Buffer GraphQL API lets developers access data efficiently by requesting exactly the fields they need. While the Buffer team aims to build a fast and efficient API, there are a few optimization techniques you can use to significantly reduce the number of requests you send and increase the speed of your integration.

## Request only the data you need

Every field you select adds complexity to your requests. Keeping it small and simple will result in faster and more efficient queries.

Select only what you use. For example, if you are building a queue view, you may not need `metrics`, `notes`, `assets` and `author` on every post. Fewer fields mean a smaller response and a faster query.


## Combine queries into a single request

One of GraphQL's most useful features is that a single query can include
multiple top-level fields. Instead of making separate requests for your
account details, your channels, and your daily posting limits, you can
fetch all three in one round trip:

```graphql
query Bootstrap {
  account {
    id
    email
    timezone
  }
  channels(input: { organizationId: "your_org_id" }) {
    id
    name
    service
  }
  dailyPostingLimits(input: { channelIds: ["your_channel_id"] }) {
    channelId
    isAtLimit
    scheduled
  }
}
```

Each top-level field resolves independently, and the response contains
all three results under a single `data` object. This is a good pattern
for app startup or dashboard views, where you'd otherwise fire several
requests at once.

Many Buffer API fields also accept lists - for example,
`dailyPostingLimits` takes an array of `channelIds` - so you can fetch
data for many resources in one call rather than looping over them
individually.

## Use aliases to run the same query more than once

Combining queries works well when each top-level field is different. But
if you request the same field twice with different arguments, the server
rejects the query - the response is keyed by field name, and there's no
way to place two different result sets under one key. Aliases solve
this by letting you rename the response key for each request.

For example, say you want your scheduled posts and your failed posts in
a single request. Alias the two `posts` fields as `scheduled` and
`failed`:

```graphql
query QueueOverview {
  scheduled: posts(
    first: 100
    input: { organizationId: "your_org_id", filter: { status: [scheduled] } }
  ) {
    edges { node { ...PostFields } }
    pageInfo { hasNextPage endCursor }
  }
  failed: posts(
    first: 100
    input: { organizationId: "your_org_id", filter: { status: [error] } }
  ) {
    edges { node { ...PostFields } }
    pageInfo { hasNextPage endCursor }
  }
}

fragment PostFields on Post {
  id
  text
  status
  dueAt
  channelId
}
```

The response contains `data.scheduled` and `data.failed`, each with its
own result set.

Aliases are also the way to look up a specific set of records in one
request, since `posts` filters on status, channel and date but not on ID.
Checking 25 posts one request at a time is 25 requests; the same 25 `post`
lookups aliased into one document is one. A query is capped at **30
aliases**, so where a filter can describe what you want, prefer the filter.

## Use fragments to simplify complex queries

Another technique that can significantly improve your interactions with the Buffer API is using fragments. The alias query above already uses one. Both `posts` fields need the same five fields on `Post`, so rather than spelling that selection out twice, it is defined once and referenced with the spread syntax (`...PostFields`):

```graphql
fragment PostFields on Post {
  id
  text
  status
  dueAt
  channelId
}
```

Anywhere those fields are needed, `...PostFields` stands in for them.

Fragments can help simplify queries, make them more readable, and reduce the maintenance overhead when adding or renaming a field.

They also keep query documents small, which matters against the
**15,000-token** document limit.

## Filter and sort on the server

Fetching a wide set and narrowing it in your own code costs you every request that returned data you threw away.

Queries like `posts` support filters on status, channel, post type, tags, and date ranges through `dueAt` and `createdAt` comparators. Using them can significantly reduce the amount of data you don't need:

```graphql
query RecentlySent {
  posts(
    first: 100
    input: {
      organizationId: "your_org_id"
      filter: {
        status: [sent]
        channelIds: ["your_channel_id"]
        dueAt: { start: "2026-01-01T00:00:00Z" }
      }
      sort: { field: dueAt, direction: desc }
    }
  ) {
    edges { node { id text dueAt channelId } }
    pageInfo { hasNextPage endCursor }
  }
}
```

## Use flat fields instead of nested objects

Some values can appear in multiple places in the schema. For example, a `Post` carries
`channelId` and `channelService` as plain fields, and the same values
are also reachable through the `channel` object. Accessing them requires different resources.

The flat fields are values the post already has - reading them costs
nothing extra. Requesting the `channel` object makes the API load the
full channel first, even if all you select inside it is `id` and
`service`.

```graphql
# Slower: loads the full channel for every post
posts(first: 100, input: { organizationId: "your_org_id" }) {
  edges { node { id channel { id service } } }
}

# Faster: reads values the post already has
posts(first: 100, input: { organizationId: "your_org_id" }) {
  edges { node { id channelId channelService } }
}
```

At 100 posts per page, that's 100 channel loads you didn't need. A good rule of thumb - check whether the value you want already exists on the object you have before reaching through to a
nested one.


## Use pagination with a large enough page

Pagination allows you to loop through pages to access more results. When you need to access a large number of results, aim to use a large enough page window. At the moment, the Buffer API supports returning up to 100 items per request.

One caveat to keep in mind - page size and selection width multiply. Every field you select is resolved once per item in the page, which counts towards the [query complexity budget](https://developers.buffer.com/guides/api-limits.md#query-complexity) and towards the size of the response.

See [Pagination](https://developers.buffer.com/guides/pagination.md) for how cursors work.

## Aggregate when possible

Some queries in the Buffer API are designed to aggregate large amounts of data. When you want totals rather than individual rows, use them instead of paging through the underlying records and adding them up yourself.

`aggregatedPostMetrics` is the clearest example. It rolls up post performance across a date range of up to 365 days and returns the totals in a single request:

```graphql
query QuarterRollup {
  aggregatedPostMetrics(
    input: {
      organizationId: "your_org_id"
      startDateTime: "2026-04-01T00:00:00Z"
      endDateTime: "2026-06-30T00:00:00Z"
    }
  ) {
    metrics { type name value unit }
    metricsUpdatedAt
  }
}
```

Every result includes a baseline of `postCount`, `reactions` and `comments`:

```json
{
  "metrics": [
    { "type": "postCount", "name": "Posts", "value": 250, "unit": "count" },
    { "type": "reactions", "name": "Reactions", "value": 6800, "unit": "count" },
    { "type": "comments", "name": "Comments", "value": 750, "unit": "count" }
  ],
  "metricsUpdatedAt": "2026-06-30T03:23:11.120Z"
}
```

Building those same numbers by hand means paging every sent post in the window, selecting `metrics` on each one, and summing them yourself - several requests and a far larger response for a result the API can return in one.

## Cache what rarely changes

Channels, organization IDs and tags mostly change when someone connects or disconnects an account. In most cases, these details don't change as dynamically as other data points. For that reason they are good candidates for client-side caching. This can drop the request volume by an order of magnitude.

A few techniques to consider:

- **Cache the organization ID and channel list, and refresh them on a schedule.** A daily refresh, or a manual "refresh channels" button in your UI, beats a fetch on every operation.
- **Avoid fetching channels just to validate a channel ID.** If you already stored the ID, use it. `createPost` returns a typed `NotFoundError` with `"Channel not found"` when an ID is wrong, so you can recover from the failure instead of paying for a lookup before every write.

GraphQL clients like [Apollo](https://www.apollographql.com/docs/react/caching/overview) can do most of this for you. Apollo's `InMemoryCache` normalizes anything with an `id`, so a channel fetched once is reused everywhere it appears. Its default `fetchPolicy` is `cache-first`, which means repeat queries are answered from memory without touching the API:

```javascript
import { ApolloClient, InMemoryCache, gql } from '@apollo/client'

const client = new ApolloClient({
  uri: 'https://api.buffer.com',
  headers: { Authorization: `Bearer ${process.env.BUFFER_API_KEY}` },
  cache: new InMemoryCache(),
})

const GET_CHANNELS = gql`
  query GetChannels {
    channels(input: { organizationId: "your_org_id" }) {
      id
      name
      service
    }
  }
`

// First call goes to the API. Later calls are served from the cache.
const { data } = await client.query({ query: GET_CHANNELS })

// Ask for fresh data only when it matters, such as after someone
// connects or disconnects a channel.
await client.query({ query: GET_CHANNELS, fetchPolicy: 'network-only' })
```

The pattern holds whichever client you use: read from the cache by default, and go to the network deliberately rather than on every operation.

## Poll less, and poll narrowly

There are no webhooks, so keeping data in sync means polling. Make each poll cheap and infrequent.

Ask only for what changed. Keep a checkpoint of the last time you synced, and filter on `createdAt: { start: $checkpoint }` rather than walking the full history each run. Cursors are tied to the result set that produced them, so store the timestamp, not the cursor, between runs.

## Prompts for AI agents

Buffer has an [MCP server](https://developers.buffer.com/guides/integrations/mcp.md), so the same API is available to Claude, Cursor, ChatGPT and other assistants. The quota does not change when you reach it that way: MCP connections and your personal API keys share one rate-limit bucket, so connecting another assistant does not give you more room.

The server already steers the agent quite a bit on its own. Its recommended workflow starts at `get_account` for the organization, moves to `list_channels` for channels, and routes summaries and averages to `get_aggregated_post_metrics`. Its tool descriptions tell the agent to prefer the domain tools over raw GraphQL, and to take field and enum names from the schema rather than from memory.

So the prompts below deliberately do not repeat any of that. They cover the habits the server does not describe, and they belong in your system prompt, your project instructions, or an `AGENTS.md` file, so they apply to every session rather than being repeated by hand.

### Load the context once

The server tells the agent to start with `get_account` and `list_channels`. It does not say to stop calling them, and an agent that re-lists organizations and channels every turn spends the session relearning what it already knew.

```text
Call get_account and list_channels once at the start of this session.
Keep the organization ID and channel list in context, and reuse them
throughout the session.

Do not fetch them again unless I tell you the channels have changed.
```

### Say what an ID looks like

The server says where to get IDs. It does not say what a valid one looks like, and invented IDs are a large share of failed calls.

```text
Buffer IDs are opaque 24-character hex strings.

A post's ID on the social network is not its Buffer ID. Placeholders
such as "default" or "me" do not exist.

If you do not have an ID from get_account or list_channels, ask me
for it rather than constructing one.
```

### Ask for one page at a time

```text
When listing posts, filter by status and date range. Request at most
100 items per page.

Fetch one page and show me the result. Only fetch another page when I
ask for it.
```

### Combine unrelated reads

Nothing in the server suggests this, and it is the technique that saves the most in a single session.

```text
When you are already using execute_query and need several unrelated
things at once, ask for them in a single document.

Use multiple root fields for different queries.

Use aliases when the same field repeats with different arguments.
```

### Fail once, not repeatedly

Every request counts against your quota whether it succeeds or fails. Only a `429` is refunded, so a retry loop on a broken assumption is pure waste.

```text
If a call fails, read the error message and fix the specific thing it
names. Retry at most once.

If it fails again, stop and tell me what went wrong instead of trying
variations.

If a call returns a 429, stop and report the Retry-After wait time.
Do not retry in a loop.
```

Unattended code should behave differently here: back off and retry as described in [Rate Limits](https://developers.buffer.com/guides/api-limits.md). The instruction above is for an interactive agent, where surfacing the wait to a human beats sleeping through it.

### One block to paste

To set this up once rather than prompt by prompt:

```text
When using the Buffer MCP server:

1. Call get_account and list_channels once per session.
   Reuse the organization ID and channel list; do not fetch them again.
2. Buffer IDs are opaque 24-character hex strings.
   A post's ID on a social network is not its Buffer ID.
   Placeholders such as "default" do not exist.
   Ask me for an ID rather than inventing one.
3. When listing posts, filter by status and date range. Request at
   most 100 items per page, and fetch one page at a time.
4. When you need several unrelated things from execute_query,
   ask for them in one document, using root fields or aliases.
5. If a call fails, fix what the error names and retry at most once.
   Then stop and tell me. On a 429, report the Retry-After wait.
```

## Next steps

- [Rate Limits](https://developers.buffer.com/guides/api-limits.md): the quotas, the headers, and how to back off
- [MCP](https://developers.buffer.com/guides/integrations/mcp.md): connect any MCP-compatible AI tool to Buffer
- [Pagination](https://developers.buffer.com/guides/pagination.md): how cursors work
- [Content Items](https://developers.buffer.com/guides/content-items.md): create posts for many channels in one call
- [Post Metrics](https://developers.buffer.com/guides/post-metrics.md): per-post metrics and aggregation
- [Aggregate Post Metrics](https://developers.buffer.com/examples/aggregate-post-metrics.md): a working rollup query
