Skip to main content

Performance & Optimization

At scale, the difference between a well-optimised API integration and a naive one can mean the difference between sub-second dashboards and timeouts, or staying within rate limits versus being blocked. This guide covers the patterns that matter most when building against the Lumar GraphQL API.

Pagination page size

All connection fields accept a first or last argument between 1 and 1,000. Choosing the right page size depends on your use case:

Use caseRecommended page size
UI rendering20–100
Data export / processing100–500

Always use pageInfo.hasNextPage combined with endCursor to drive iteration. Do not guess at offsets or rely on totalCount alone to calculate page boundaries.

Here is a complete example of iterating through all projects in an account:

Operation: query GetAllProjects($accountId: ObjectID!, $after: String) { getAccount(id: $accountId) { projects(first: 100, after: $after) { nodes { id name } pageInfo { hasNextPage endCursor } totalCount } } }Variables: { "accountId": "TjAwN0FjY291bnQxMjM0" }Response Example: { "data": { "getAccount": { "projects": { "nodes": [ { "id": "TjAwN1Byb2plY3Q2MTMy", "name": "Main website" }, { "id": "TjAwN1Byb2plY3Q2MTMz", "name": "Blog" } ], "pageInfo": { "hasNextPage": true, "endCursor": "Mg" }, "totalCount": 47 } } } }
GetAllProjectsTry in Explorer
GraphQL
query GetAllProjects($accountId: ObjectID!, $after: String) {
getAccount(id: $accountId) {
projects(first: 100, after: $after) {
nodes {
id
name
}
pageInfo {
hasNextPage
endCursor
}
totalCount
}
}
}

Loop through all pages in TypeScript:

async function fetchAllProjects(accountId: string): Promise<Project[]> {
const allProjects: Project[] = [];
let after: string | null = null;
let hasNextPage = true;

while (hasNextPage) {
const result = await executeQuery(GET_ALL_PROJECTS, { accountId, after });
const { nodes, pageInfo } = result.data.getAccount.projects;

allProjects.push(...nodes);
hasNextPage = pageInfo.hasNextPage;
after = pageInfo.endCursor;
}

return allProjects;
}
note

Fetch totalCount on the first page only — omit it from subsequent requests to avoid the extra computation on every page.

Prefer nodes over edges when you don't need cursors

GraphQL connections expose two equivalent ways to access items. nodes is shorthand for edges.node and produces cleaner, less verbose queries. Use it by default.

# Simpler — use when you don't need per-item cursors
nodes {
id
name
}

# Use when you need individual item cursors
edges {
cursor
node {
id
name
}
}

Only reach for edges when you need the per-item cursor value — for example, when resuming a partially-completed export from a specific position in a large result set.

Batching multiple operations

The API supports request batching — multiple GraphQL operations sent in a single HTTP request as a JSON array. This reduces round trips and TCP connection overhead when you have several independent operations to fire at once.

When using Apollo Client, configure BatchHttpLink:

import { BatchHttpLink } from "@apollo/client/link/batch-http";

const batchLink = new BatchHttpLink({
uri: "https://api.lumar.io/graphql",
batchMax: 25, // up to 25 operations per request
batchInterval: 20, // wait 20ms to collect operations before sending
});
note

The batched request body is a JSON array. The Lumar API supports this format natively — no extra configuration is required on the server side.

Good use cases for batching:

  • Creating or deleting many alerts in a single user action.
  • Fetching multiple independent resources (e.g. several crawl statuses) in parallel without composing a multi-root query.

Polling for live crawl status

When monitoring an active crawl, poll at intervals appropriate to the current state rather than opening a persistent connection:

StatePoll interval
Active crawlevery 5 seconds
Idle / background monitoringevery 60 seconds

Always stop polling on error to avoid hammering a failing endpoint.

// Apollo Client example
const { startPolling, stopPolling } = useQuery(GET_CRAWL_STATUS, {
variables: { crawlId },
onError: () => stopPolling(),
});

useEffect(() => {
if (crawlIsActive) {
startPolling(5000); // 5s while crawl is running
} else {
stopPolling();
}
return () => stopPolling();
}, [crawlIsActive]);
warning

Do not poll at short intervals (under 5 seconds) for non-critical status checks. Sustained high-frequency polling across multiple crawls can push you toward the rate limit of 6,000 requests per 5-minute window.

Reduce round trips with multi-root queries

GraphQL allows multiple root fields in a single query. Instead of making three separate HTTP requests for account information, a project list, and reference data, combine them:

Operation: query DashboardBootstrap($accountId: ObjectID!) { getAccount(id: $accountId) { name subscription { plan { name } } projects(first: 20) { nodes { id name primaryDomain } totalCount } } getReportTemplates { nodes { id name } } }Variables: { "accountId": "TjAwN0FjY291bnQxMjM0" }Response Example: { "data": { "getAccount": { "name": "Acme Corp", "subscription": { "plan": { "name": "Professional" } }, "projects": { "nodes": [ { "id": "TjAwN1Byb2plY3Q2MTMy", "name": "Main website", "primaryDomain": "https://example.com" } ], "totalCount": 12 } }, "getReportTemplates": { "nodes": [{ "id": "TjAwOFJlcG9ydFRlbXBsYXRlMQ", "name": "SEO Overview" }] } } }
DashboardBootstrapTry in Explorer
GraphQL
query DashboardBootstrap($accountId: ObjectID!) {
getAccount(id: $accountId) {
name
subscription {
plan {
name
}
}
projects(first: 20) {
nodes {
id
name
primaryDomain
}
totalCount
}
}
getReportTemplates {
nodes {
id
name
}
}
}

This pattern is especially valuable on application startup or page load, where latency is most noticeable.

tip

See the Advanced Query Patterns page for more examples of composing efficient multi-root queries.