Skip to main content

Performance & Best Practices

This guide covers strategies for optimizing your use of the SensorUp GraphQL API to achieve the best performance and reliability.

Query Optimization

Request Only What You Need

GraphQL’s strength is precise field selection. Use it: Bad:
query {
  assets {
    all(first: 100) {
      edges {
        node {
          id
          profile
          displayName
          secondaryDisplayName
          modifiedAt
          geometry { type coordinates bbox }
          properties           # Potentially large!
          observation         # Very large!
          geoJSONFeature      # Redundant with above
          related             # Expensive!
        }
      }
    }
  }
}
Good:
query {
  assets {
    all(first: 100) {
      edges {
        node {
          id
          displayName
          modifiedAt
        }
      }
    }
  }
}
Impact: Reduced payload size by 90%+, faster response times

Use Pagination Appropriately

Don’t request more data than you can display: Bad:
query {
  assets {
    all(first: 10000) {  # Too many!
      edges {
        node {
          id
          displayName
        }
      }
    }
  }
}
Good:
query {
  assets {
    all(first: 20) {    # One page
      edges {
        node {
          id
          displayName
        }
      }
      pageInfo {
        hasNextPage
        endCursor
      }
    }
  }
}
Recommended page sizes:
  • UI lists: 20-50 items
  • Background processing: 100-500 items
  • Exports: Use export endpoints, not pagination
GraphQL allows querying multiple resources in one request: Bad:
// 3 separate requests
const session = await getSession()
const assets = await getAssets()
const issues = await getIssues()
Good:
// 1 request
const result = await query(`
  query {
    session {
      username
      authenticated
    }
    assetProfiles {
      all(first: 20) {
        edges {
          node {
            profile
            title
          }
        }
      }
    }
    issues {
      all(first: 20) {
        edges {
          node {
            id
            title
          }
        }
      }
    }
  }
`)
Impact: Reduced latency by ~66%, fewer round trips

Use Fragments for Reusability

Define reusable fragments for common field sets:
fragment AssetSummary on Asset {
  id
  displayName
  modifiedAt
  geometry {
    type
    coordinates
  }
}

fragment IssueSummary on Issue {
  id
  title
  status
  severity
  dueAt
}

query GetData {
  assets {
    all(first: 20) {
      edges {
        node {
          ...AssetSummary
        }
      }
    }
  }
  issues {
    all(first: 20) {
      edges {
        node {
          ...IssueSummary
        }
      }
    }
  }
}

Federation Performance

Understand Query Plans

Cross-subgraph queries require multiple fetches. Minimize hops: Less Efficient:
query {
  issues {
    all(first: 100) {
      edges {
        node {
          subject {
            assetReference {    # Fetch from su-assets (100 times)
              asset {
                displayName
                assetProfile {   # Another fetch (100 times)
                  title
                }
              }
            }
          }
        }
      }
    }
  }
}
More Efficient:
query {
  issues {
    all(first: 100) {
      edges {
        node {
          id
          title
          status
        }
      }
    }
  }
}

// If you need asset details, fetch them separately with specific IDs

Leverage Parallel Execution

Federation executes independent root fields in parallel:
query {
  session {          # su-auth - parallel
    username
  }
  assetProfiles {    # su-assets - parallel
    all(first: 10) {
      edges {
        node {
          title
        }
      }
    }
  }
  issues {           # su-issues - parallel
    all(first: 10) {
      edges {
        node {
          title
        }
      }
    }
  }
}
All three subgraphs are queried simultaneously.

Filtering & Searching

Use Server-Side Filtering

Filter on the server, not the client: Bad:
// Fetch everything, filter client-side
const all = await getAssets({ first: 1000 })
const filtered = all.filter(a => a.modifiedAt > cutoff)
Good:
// Filter server-side
const filtered = await getAssets({
  first: 100,
  filter: { modifiedAfter: cutoff }
})

Specific Lookups vs. List Filtering

Use ID lookups when you know the ID: Less Efficient:
query {
  assets {
    all(first: 1000, filter: { id: "asset-123" }) {
      edges {
        node {
          id
          displayName
        }
      }
    }
  }
}
More Efficient:
query {
  assetProfiles {
    byId(profile: "wells") {
      assetById(id: "asset-123") {
        id
        displayName
      }
    }
  }
}

Caching Strategies

Cache Static Data

Some data changes infrequently:
Data TypeChange FrequencyCache Duration
Asset ProfilesRarely1 hour - 1 day
Catalog EntriesRarely1 day - 1 week
Form TemplatesOccasionally1 hour
Map ConfigurationsRarely1 day
User SessionOftenNo cache (always fetch)
AssetsOftenNo cache or short (5 min)

HTTP Caching

Configure appropriate cache headers:
const response = await fetch(url, {
  headers: {
    'Cache-Control': 'max-age=300' // 5 minutes
  }
})

Application-Level Caching

Use a caching client like Apollo Client:
import { ApolloClient, InMemoryCache } from '@apollo/client'

const client = new ApolloClient({
  uri: 'https://customer-demo.sensorup.com/api/graphql',
  cache: new InMemoryCache({
    typePolicies: {
      AssetProfile: {
        keyFields: ['profile']
      },
      Asset: {
        keyFields: ['profile', 'id']
      }
    }
  })
})

Rate Limiting & Throttling

Implement Exponential Backoff

When encountering rate limits or errors:
async function fetchWithRetry(query, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    try {
      return await fetch(query)
    } catch (error) {
      if (i === maxRetries - 1) throw error

      const delay = Math.min(1000 * Math.pow(2, i), 10000)
      await new Promise(resolve => setTimeout(resolve, delay))
    }
  }
}

Batch Updates

Instead of many small mutations, batch when possible: Less Efficient:
for (const issue of issues) {
  await updateIssue(issue.id, { status: 'CLOSED' })
}
More Efficient:
await bulkUpdateIssues({
  filter: { issueIds: issues.map(i => i.id) },
  updates: { status: 'CLOSED' }
})

Query Complexity

Avoid Deep Nesting

Deep queries can be expensive: High Complexity:
query {
  sites {
    all {
      assets {
        all {
          issueSubject {
            issues {
              all {
                annotations {
                  all {
                    # Very expensive!
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}
Lower Complexity:
query {
  sites {
    all(first: 20) {
      edges {
        node {
          id
          name
        }
      }
    }
  }
}

# Then fetch assets for specific sites
# Then fetch issues for specific assets

Limit List Sizes

Always use pagination limits:
query {
  assets {
    all(first: 20) {  # Always specify!
      edges {
        node {
          id
        }
      }
    }
  }
}

Monitoring & Debugging

Use Correlation IDs

Include correlation IDs for tracking:
const correlationId = uuid()

await mutation({
  variables: {
    input: { ... },
    correlationId
  }
})

console.log(`Request ID: ${correlationId}`)

Log Query Performance

Track slow queries:
const start = Date.now()
const result = await client.query({ query })
const duration = Date.now() - start

if (duration > 1000) {
  console.warn(`Slow query: ${duration}ms`, query)
}

Monitor Error Rates

Track and alert on error patterns:
let errorCount = 0
let requestCount = 0

try {
  await query()
  requestCount++
} catch (error) {
  errorCount++
  requestCount++

  const errorRate = errorCount / requestCount
  if (errorRate > 0.05) {
    alert('High error rate detected!')
  }
}

Network Optimization

Use Compression

Enable gzip/brotli compression:
fetch(url, {
  headers: {
    'Accept-Encoding': 'gzip, deflate, br'
  }
})

Minimize Payload Size

  • Use fragments to avoid duplication
  • Omit __typename if not needed
  • Use aliases to simplify response structure

Connection Pooling

Reuse HTTP connections:
const agent = new http.Agent({
  keepAlive: true,
  maxSockets: 50
})

fetch(url, { agent })

Best Practices Checklist

Query Design

  • Request only needed fields
  • Use pagination (first: 20-100)
  • Apply server-side filters
  • Use specific lookups vs. list queries
  • Batch related queries in one request
  • Use fragments for reusability

Performance

  • Cache static data (profiles, catalogs)
  • Implement exponential backoff
  • Monitor query performance
  • Use correlation IDs
  • Minimize cross-subgraph hops
  • Limit query depth (≤ 4 levels)

Error Handling

  • Check GraphQL-level errors
  • Check mutation-level errors
  • Log errors with context
  • Implement retry logic
  • Handle partial failures gracefully

Security

  • Store credentials securely
  • Rotate API keys regularly
  • Use HTTPS always
  • Validate input data
  • Sanitize user-provided content

Performance Metrics

Target Metrics

MetricTargetGoodNeeds Improvement
Query latency (p50)< 200ms< 500ms> 500ms
Query latency (p99)< 1s< 2s> 2s
Error rate< 0.1%< 1%> 1%
Cache hit rate> 80%> 60%< 60%

Measuring Performance

const metrics = {
  queryCount: 0,
  errorCount: 0,
  totalDuration: 0,
  cacheHits: 0
}

function recordQuery(duration, fromCache, error) {
  metrics.queryCount++
  metrics.totalDuration += duration
  if (fromCache) metrics.cacheHits++
  if (error) metrics.errorCount++
}

function getMetrics() {
  return {
    avgDuration: metrics.totalDuration / metrics.queryCount,
    errorRate: metrics.errorCount / metrics.queryCount,
    cacheHitRate: metrics.cacheHits / metrics.queryCount
  }
}

Common Anti-Patterns

1. Over-fetching

# Don't fetch everything "just in case"
query {
  assets {
    all(first: 100) {
      edges {
        node {
          # Every possible field...
        }
      }
    }
  }
}

2. N+1 in Application Code

// Don't do this!
const issues = await getIssues()
for (const issue of issues) {
  const asset = await getAsset(issue.assetId)  // N queries!
}

3. Polling Too Frequently

// Don't poll every second!
setInterval(async () => {
  await fetchData()
}, 1000)

// Use reasonable intervals
setInterval(async () => {
  await fetchData()
}, 30000)  // 30 seconds

4. Ignoring Errors

// Don't ignore errors!
try {
  await mutation()
} catch (e) {
  // Silence!
}

// Handle them properly
try {
  await mutation()
} catch (error) {
  console.error('Mutation failed:', error)
  showErrorToUser(error.message)
  reportToMonitoring(error)
}

Resources

Support

For performance issues or optimization assistance:
  • Review query plans in Apollo Studio
  • Contact your SensorUp account team
  • Provide correlation IDs for specific slow queries