| name | navan-performance-tuning |
| description | Use when optimizing Navan API call patterns for high-volume integrations — caching, batching, connection pooling, and pagination strategies.
Trigger with "navan performance tuning" or "navan api optimization" or "navan caching".
|
| allowed-tools | Read, Write, Edit, Bash(curl:*), Grep, Glob |
| version | 1.7.0 |
| license | MIT |
| author | Jeremy Longshore <jeremy@intentsolutions.io> |
| tags | ["saas","navan","travel"] |
| compatibility | Designed for Claude Code |
Navan Performance Tuning
Overview
Navan's REST API has no bulk endpoints or GraphQL — every data fetch is a separate HTTP request. High-volume integrations syncing thousands of bookings, expenses, or user records quickly become bottlenecked by sequential API calls, redundant fetches, and naive pagination. This skill provides concrete optimization patterns: response caching with data-type-specific TTLs, parallel request execution with concurrency controls, cursor-based pagination handling, and HTTP connection reuse. Each pattern targets the real constraint: minimizing total API calls while staying under rate limits.
Prerequisites
- Active Navan integration with OAuth 2.0 credentials (client_credentials grant)
- Node.js 18+ (for native fetch and AbortController)
- Understanding of your data volume — bookings/day, users, expense reports/month
- API base URL:
https://api.navan.com/v1
Instructions
Step 1 — Implement Response Caching with Data-Appropriate TTLs
Different Navan data types change at different rates. Cache accordingly:
interface CacheEntry<T> {
data: T;
expires_at: number;
etag?: string;
}
const CACHE_TTL: Record<string, number> = {
'users': 3600_000,
'policies': 86400_000,
'bookings': 300_000,
'expenses': 600_000,
};
const cache = new Map<string, CacheEntry<unknown>>();
async function cachedFetch<T>(
endpoint: string,
token: string
): Promise<T> {
const cacheKey = endpoint;
const entry = cache.get(cacheKey) as CacheEntry<T> | undefined;
if (entry && entry.expires_at > Date.now()) {
entry.;
}
dataType = endpoint.()[].()[];
ttl = [dataType] ?? ;
response = (, {
: {
: ,
: ,
},
});
(!response.) {
();
}
data = response.() T;
cache.(cacheKey, {
data,
: .() + ttl,
: response..() ?? ,
});
data;
}
Step 2 — Parallel Fetch with Concurrency Throttling
Fetch multiple resources concurrently without overwhelming rate limits:
async function parallelFetch<T>(
endpoints: string[],
token: string,
concurrency: number = 5
): Promise<T[]> {
const results: T[] = [];
const queue = [...endpoints];
async function worker(): Promise<void> {
while (queue.length > 0) {
const endpoint = queue.shift()!;
try {
const data = await cachedFetch<T>(endpoint, token);
results.push(data);
} catch (err) {
const status = (err as Error).message.match(/(\d{3})/)?.[1];
if (status === '429') {
queue.unshift(endpoint);
await new Promise(r => setTimeout(r, 2000));
} else {
throw err;
}
}
}
}
workers = .(
{ : .(concurrency, endpoints.) },
()
);
.(workers);
results;
}
userIds = [, , ];
profiles = (
userIds.( ),
token,
);
Step 3 — Efficient Cursor-Based Pagination
Page through large result sets without missing or duplicating records:
async function* paginateAll<T>(
endpoint: string,
token: string,
pageSize: number = 50
): AsyncGenerator<T[]> {
let page = 0;
while (true) {
const params = new URLSearchParams({
page: String(page),
size: String(pageSize),
});
const url = `https://api.navan.com/v1/${endpoint}?${params}`;
const response = await fetch(url, {
headers: { 'Authorization': `Bearer ${token}` },
});
if (!response.ok) {
throw new Error(`Navan API ${response.status} on ${endpoint}`);
}
const body = await response.json();
const items: T[] = body.data ?? [];
if (items.length === 0) break;
yield items;
if (items. < pageSize) ;
page++;
}
}
totalProcessed = ;
( page (, token, )) {
(page);
totalProcessed += page.;
.();
}
Step 4 — HTTP Connection Reuse
Keep TCP connections alive across multiple API calls:
import { Agent } from 'undici';
const navanAgent = new Agent({
keepAliveTimeout: 30_000,
keepAliveMaxTimeout: 60_000,
connections: 10,
pipelining: 1,
});
const response = await fetch('https://api.navan.com/v1/bookings', {
headers: { 'Authorization': `Bearer ${token}` },
dispatcher: navanAgent,
});
Output
Optimized Navan API integration with:
- 60-80% fewer API calls through intelligent caching
- 5-10x faster sync jobs via parallel execution
- Zero missed records with robust cursor pagination
- Lower latency from connection reuse and keep-alive
Error Handling
| HTTP Code | Meaning | Performance Action |
|---|
200 | Success | Cache the response with appropriate TTL |
304 | Not Modified | Use cached version (ETag match) |
401 | Token expired | Refresh token, retry once, do not cache |
429 | Rate limited | Exponential backoff: 1s, 2s, 4s — max 3 retries |
500 | Server error | Retry once after 5s, skip on second failure |
503 | Service unavailable | Pause all workers for 30s, then resume |
Examples
Before and after optimization for a 10,000-booking sync:
Before (naive sequential):
API calls: 10,000 (one per booking)
Time: 45 minutes
Rate limit hits: 12
After (cached + parallel + paginated):
API calls: 200 (pages of 50)
Time: 4 minutes
Rate limit hits: 0
Cache invalidation on webhook event:
function handleWebhook(event: { type: string; booking_id: string }) {
if (event.type === 'booking.updated') {
cache.delete(`bookings/${event.booking_id}`);
}
}
Resources
Next Steps
- Add
navan-rate-limits for detailed rate limit handling strategies
- Add
navan-cost-tuning to optimize the business cost side alongside API performance
- See
navan-observability to measure the impact of these optimizations