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.