Retry Strategies for ChatGPT Apps: Exponential Backoff
Building production-grade ChatGPT apps requires robust error handling. When your app makes tool calls to external APIs, network failures, rate limits, and transient errors are inevitable. Without proper retry strategies, your users face frustrating failures and your app appears unreliable.
This comprehensive guide shows you how to implement exponential backoff, jittered retries, idempotency checking, dead letter queues, and retry budgets—the five pillars of resilient ChatGPT app architecture.
Why Exponential Backoff Matters for ChatGPT Apps
ChatGPT apps operate in a unique environment where the model may retry tool calls automatically if it doesn't receive a timely response. Your MCP server must handle:
- Rate limiting: OpenAI and your backend APIs have strict rate limits
- Network transience: Temporary network failures during tool execution
- Thundering herd: Multiple concurrent users triggering simultaneous API calls
- Model retries: ChatGPT itself may retry if responses are slow or incomplete
- Resource contention: Shared databases and services experiencing temporary load
Learn more about ChatGPT app architecture best practices to understand the full context of why retry strategies are critical.
The Five Pillars of Retry Resilience
1. Exponential Backoff with Jitter
Exponential backoff increases wait time between retries exponentially (1s, 2s, 4s, 8s, 16s). Jitter adds randomness to prevent synchronized retry storms.
Production Retry Handler (120 lines)
// retry-handler.ts
import { v4 as uuidv4 } from 'uuid';
interface RetryConfig {
maxRetries: number;
baseDelayMs: number;
maxDelayMs: number;
jitterFactor: number;
retryableErrors: string[];
timeout: number;
}
interface RetryContext {
attempt: number;
requestId: string;
startTime: number;
lastError?: Error;
backoffDelays: number[];
}
export class RetryHandler {
private config: RetryConfig;
private budgetTracker: RetryBudgetTracker;
constructor(config: Partial<RetryConfig> = {}) {
this.config = {
maxRetries: config.maxRetries ?? 5,
baseDelayMs: config.baseDelayMs ?? 1000,
maxDelayMs: config.maxDelayMs ?? 32000,
jitterFactor: config.jitterFactor ?? 0.3,
retryableErrors: config.retryableErrors ?? [
'ECONNRESET',
'ETIMEDOUT',
'ENOTFOUND',
'RATE_LIMITED',
'SERVICE_UNAVAILABLE'
],
timeout: config.timeout ?? 30000
};
this.budgetTracker = new RetryBudgetTracker();
}
/**
* Execute operation with exponential backoff and jitter
*/
async executeWithRetry<T>(
operation: () => Promise<T>,
context?: Partial<RetryContext>
): Promise<T> {
const ctx: RetryContext = {
attempt: 0,
requestId: context?.requestId ?? uuidv4(),
startTime: Date.now(),
backoffDelays: []
};
while (ctx.attempt <= this.config.maxRetries) {
try {
// Check if we've exceeded timeout
const elapsed = Date.now() - ctx.startTime;
if (elapsed > this.config.timeout) {
throw new Error(`Operation timeout after ${elapsed}ms`);
}
// Check retry budget
if (!this.budgetTracker.canRetry()) {
throw new Error('Retry budget exceeded');
}
// Execute operation
const result = await operation();
// Success - record metrics
this.budgetTracker.recordSuccess(ctx.attempt);
console.log(`[${ctx.requestId}] Success after ${ctx.attempt} retries`);
return result;
} catch (error) {
ctx.lastError = error as Error;
ctx.attempt++;
// Check if error is retryable
if (!this.isRetryable(error)) {
console.error(`[${ctx.requestId}] Non-retryable error:`, error);
throw error;
}
// Max retries exceeded
if (ctx.attempt > this.config.maxRetries) {
console.error(
`[${ctx.requestId}] Max retries (${this.config.maxRetries}) exceeded`
);
this.budgetTracker.recordFailure();
throw new Error(
`Max retries exceeded: ${ctx.lastError.message}`
);
}
// Calculate backoff with jitter
const delay = this.calculateBackoff(ctx.attempt);
ctx.backoffDelays.push(delay);
console.warn(
`[${ctx.requestId}] Retry ${ctx.attempt}/${this.config.maxRetries} ` +
`after ${delay}ms. Error: ${ctx.lastError.message}`
);
// Wait before retry
await this.sleep(delay);
}
}
throw new Error('Unexpected retry loop exit');
}
/**
* Calculate exponential backoff with jitter
*/
private calculateBackoff(attempt: number): number {
// Exponential: baseDelay * 2^(attempt-1)
const exponentialDelay = this.config.baseDelayMs * Math.pow(2, attempt - 1);
// Cap at maxDelay
const cappedDelay = Math.min(exponentialDelay, this.config.maxDelayMs);
// Add jitter: random value between [delay * (1 - jitter), delay * (1 + jitter)]
const jitterRange = cappedDelay * this.config.jitterFactor;
const jitter = (Math.random() * 2 - 1) * jitterRange;
return Math.max(0, Math.round(cappedDelay + jitter));
}
/**
* Check if error is retryable
*/
private isRetryable(error: any): boolean {
// Check error code
if (error.code && this.config.retryableErrors.includes(error.code)) {
return true;
}
// Check HTTP status codes
if (error.response?.status) {
const status = error.response.status;
// Retry on 429 (rate limit), 500, 502, 503, 504
if ([429, 500, 502, 503, 504].includes(status)) {
return true;
}
}
// Check error message
if (error.message) {
const retryableMessages = [
'timeout',
'rate limit',
'temporarily unavailable',
'connection reset'
];
const msg = error.message.toLowerCase();
return retryableMessages.some(pattern => msg.includes(pattern));
}
return false;
}
private sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
Explore our AI Conversational Editor to build ChatGPT apps with built-in retry resilience—no coding required.
2. Backoff Calculator with Multiple Strategies
Different scenarios require different backoff strategies. Here's a production-ready calculator supporting multiple algorithms.
Advanced Backoff Calculator (130 lines)
// backoff-calculator.ts
export type BackoffStrategy =
| 'exponential'
| 'linear'
| 'fibonacci'
| 'polynomial'
| 'decorrelated-jitter';
export interface BackoffConfig {
strategy: BackoffStrategy;
baseDelayMs: number;
maxDelayMs: number;
multiplier: number;
jitterEnabled: boolean;
jitterFactor: number;
}
export class BackoffCalculator {
private config: BackoffConfig;
private lastDelay: number = 0;
constructor(config: Partial<BackoffConfig> = {}) {
this.config = {
strategy: config.strategy ?? 'exponential',
baseDelayMs: config.baseDelayMs ?? 1000,
maxDelayMs: config.maxDelayMs ?? 60000,
multiplier: config.multiplier ?? 2,
jitterEnabled: config.jitterEnabled ?? true,
jitterFactor: config.jitterFactor ?? 0.3
};
}
/**
* Calculate next backoff delay
*/
calculateDelay(attempt: number): number {
let delay: number;
switch (this.config.strategy) {
case 'exponential':
delay = this.exponentialBackoff(attempt);
break;
case 'linear':
delay = this.linearBackoff(attempt);
break;
case 'fibonacci':
delay = this.fibonacciBackoff(attempt);
break;
case 'polynomial':
delay = this.polynomialBackoff(attempt);
break;
case 'decorrelated-jitter':
delay = this.decorrelatedJitter();
break;
default:
delay = this.exponentialBackoff(attempt);
}
// Apply jitter if enabled
if (this.config.jitterEnabled && this.config.strategy !== 'decorrelated-jitter') {
delay = this.applyJitter(delay);
}
// Cap at max delay
delay = Math.min(delay, this.config.maxDelayMs);
// Store for decorrelated jitter
this.lastDelay = delay;
return Math.round(delay);
}
/**
* Exponential backoff: baseDelay * multiplier^(attempt-1)
*/
private exponentialBackoff(attempt: number): number {
return this.config.baseDelayMs * Math.pow(this.config.multiplier, attempt - 1);
}
/**
* Linear backoff: baseDelay * attempt
*/
private linearBackoff(attempt: number): number {
return this.config.baseDelayMs * attempt;
}
/**
* Fibonacci backoff: delays follow Fibonacci sequence
*/
private fibonacciBackoff(attempt: number): number {
const fib = this.fibonacci(attempt);
return this.config.baseDelayMs * fib;
}
/**
* Polynomial backoff: baseDelay * attempt^2
*/
private polynomialBackoff(attempt: number): number {
return this.config.baseDelayMs * Math.pow(attempt, 2);
}
/**
* Decorrelated jitter: random between baseDelay and lastDelay * 3
* Recommended by AWS: https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/
*/
private decorrelatedJitter(): number {
const base = this.config.baseDelayMs;
const temp = Math.min(this.config.maxDelayMs, this.lastDelay * 3);
return Math.random() * (temp - base) + base;
}
/**
* Apply full jitter: random value in [0, delay]
*/
private applyJitter(delay: number): number {
const jitterRange = delay * this.config.jitterFactor;
const jitter = (Math.random() * 2 - 1) * jitterRange;
return Math.max(0, delay + jitter);
}
/**
* Calculate nth Fibonacci number
*/
private fibonacci(n: number): number {
if (n <= 1) return 1;
let a = 1, b = 1;
for (let i = 2; i <= n; i++) {
[a, b] = [b, a + b];
}
return b;
}
/**
* Get backoff sequence for visualization
*/
getSequence(maxAttempts: number): number[] {
const sequence: number[] = [];
this.lastDelay = 0; // Reset for decorrelated jitter
for (let i = 1; i <= maxAttempts; i++) {
sequence.push(this.calculateDelay(i));
}
return sequence;
}
}
// Usage example
const calculator = new BackoffCalculator({
strategy: 'exponential',
baseDelayMs: 1000,
maxDelayMs: 32000,
jitterEnabled: true
});
console.log('Backoff sequence:', calculator.getSequence(7));
// Output: [1200, 2100, 4300, 7800, 16500, 32000, 32000]
See how MakeAIHQ handles retries automatically in production ChatGPT apps.
3. Idempotency Checker for Safe Retries
Idempotency ensures repeated operations produce the same result. Critical for payment processing, data mutations, and stateful operations.
Production Idempotency Checker (110 lines)
// idempotency-checker.ts
import { createHash } from 'crypto';
interface IdempotencyRecord {
key: string;
requestHash: string;
response: any;
createdAt: number;
expiresAt: number;
}
export class IdempotencyChecker {
private cache: Map<string, IdempotencyRecord>;
private ttlMs: number;
private cleanupInterval: NodeJS.Timer;
constructor(ttlMs: number = 86400000) { // 24 hours default
this.cache = new Map();
this.ttlMs = ttlMs;
// Cleanup expired records every 5 minutes
this.cleanupInterval = setInterval(() => this.cleanup(), 300000);
}
/**
* Generate idempotency key from request
*/
generateKey(userId: string, operation: string, params: any): string {
const payload = JSON.stringify({ userId, operation, params });
return createHash('sha256').update(payload).digest('hex');
}
/**
* Check if request is duplicate
*/
async checkIdempotency(
idempotencyKey: string,
requestData: any
): Promise<{ isDuplicate: boolean; cachedResponse?: any }> {
const record = this.cache.get(idempotencyKey);
if (!record) {
return { isDuplicate: false };
}
// Check expiration
if (Date.now() > record.expiresAt) {
this.cache.delete(idempotencyKey);
return { isDuplicate: false };
}
// Verify request hash matches (prevents key collision)
const requestHash = this.hashRequest(requestData);
if (requestHash !== record.requestHash) {
throw new Error('Idempotency key conflict: different request data');
}
console.log(`[Idempotency] Cache hit for key: ${idempotencyKey}`);
return {
isDuplicate: true,
cachedResponse: record.response
};
}
/**
* Store successful response
*/
async storeResponse(
idempotencyKey: string,
requestData: any,
response: any
): Promise<void> {
const now = Date.now();
const record: IdempotencyRecord = {
key: idempotencyKey,
requestHash: this.hashRequest(requestData),
response,
createdAt: now,
expiresAt: now + this.ttlMs
};
this.cache.set(idempotencyKey, record);
console.log(`[Idempotency] Stored response for key: ${idempotencyKey}`);
}
/**
* Hash request data for verification
*/
private hashRequest(data: any): string {
const payload = JSON.stringify(data);
return createHash('sha256').update(payload).digest('hex');
}
/**
* Clean up expired records
*/
private cleanup(): void {
const now = Date.now();
let expired = 0;
for (const [key, record] of this.cache.entries()) {
if (now > record.expiresAt) {
this.cache.delete(key);
expired++;
}
}
if (expired > 0) {
console.log(`[Idempotency] Cleaned up ${expired} expired records`);
}
}
/**
* Get cache statistics
*/
getStats(): { size: number; oldestRecord: number | null } {
let oldest: number | null = null;
for (const record of this.cache.values()) {
if (oldest === null || record.createdAt < oldest) {
oldest = record.createdAt;
}
}
return {
size: this.cache.size,
oldestRecord: oldest
};
}
destroy(): void {
clearInterval(this.cleanupInterval);
this.cache.clear();
}
}
Learn about ChatGPT app security best practices including idempotency implementation.
4. Dead Letter Queue Manager
When retries are exhausted, failed operations need systematic handling. Dead letter queues (DLQs) capture failed operations for manual review and replay.
DLQ Manager (100 lines)
// dlq-manager.ts
import { v4 as uuidv4 } from 'uuid';
interface DLQEntry {
id: string;
operation: string;
payload: any;
error: {
message: string;
code?: string;
stack?: string;
};
attempts: number;
firstAttempt: number;
lastAttempt: number;
metadata: Record<string, any>;
}
export class DLQManager {
private queue: Map<string, DLQEntry>;
private maxSize: number;
constructor(maxSize: number = 10000) {
this.queue = new Map();
this.maxSize = maxSize;
}
/**
* Add failed operation to DLQ
*/
async enqueue(
operation: string,
payload: any,
error: Error,
attempts: number,
metadata: Record<string, any> = {}
): Promise<string> {
// Prevent queue overflow
if (this.queue.size >= this.maxSize) {
console.error('[DLQ] Queue full, dropping oldest entry');
this.dropOldest();
}
const entry: DLQEntry = {
id: uuidv4(),
operation,
payload,
error: {
message: error.message,
code: (error as any).code,
stack: error.stack
},
attempts,
firstAttempt: metadata.firstAttempt ?? Date.now(),
lastAttempt: Date.now(),
metadata
};
this.queue.set(entry.id, entry);
console.error(
`[DLQ] Enqueued failed operation: ${operation} (ID: ${entry.id})`
);
return entry.id;
}
/**
* Retrieve DLQ entry by ID
*/
async getEntry(id: string): Promise<DLQEntry | null> {
return this.queue.get(id) ?? null;
}
/**
* Get all entries for an operation
*/
async getEntriesByOperation(operation: string): Promise<DLQEntry[]> {
return Array.from(this.queue.values()).filter(
entry => entry.operation === operation
);
}
/**
* Replay failed operation
*/
async replay(
id: string,
executor: (payload: any) => Promise<any>
): Promise<{ success: boolean; result?: any; error?: Error }> {
const entry = this.queue.get(id);
if (!entry) {
throw new Error(`DLQ entry not found: ${id}`);
}
try {
const result = await executor(entry.payload);
this.queue.delete(id);
console.log(`[DLQ] Successfully replayed entry: ${id}`);
return { success: true, result };
} catch (error) {
console.error(`[DLQ] Replay failed for entry: ${id}`, error);
return { success: false, error: error as Error };
}
}
/**
* Drop oldest entry (FIFO)
*/
private dropOldest(): void {
const oldest = Array.from(this.queue.entries()).sort(
([, a], [, b]) => a.firstAttempt - b.firstAttempt
)[0];
if (oldest) {
this.queue.delete(oldest[0]);
}
}
/**
* Get DLQ statistics
*/
getStats(): {
size: number;
byOperation: Record<string, number>;
} {
const byOperation: Record<string, number> = {};
for (const entry of this.queue.values()) {
byOperation[entry.operation] = (byOperation[entry.operation] ?? 0) + 1;
}
return {
size: this.queue.size,
byOperation
};
}
/**
* Clear all entries
*/
clear(): void {
this.queue.clear();
}
}
Discover how MakeAIHQ's monitoring dashboard tracks failed operations in real-time.
5. Retry Budget Tracker
Retry budgets prevent retry storms from overwhelming your infrastructure. Track retry rates and enforce limits.
Retry Budget Tracker (80 lines)
// retry-budget-tracker.ts
interface BudgetConfig {
windowMs: number;
maxRetryRate: number;
minSuccessRate: number;
}
interface BudgetWindow {
startTime: number;
totalRequests: number;
totalRetries: number;
successfulRequests: number;
failedRequests: number;
}
export class RetryBudgetTracker {
private config: BudgetConfig;
private currentWindow: BudgetWindow;
constructor(config: Partial<BudgetConfig> = {}) {
this.config = {
windowMs: config.windowMs ?? 60000, // 1 minute
maxRetryRate: config.maxRetryRate ?? 0.3, // 30% max retry rate
minSuccessRate: config.minSuccessRate ?? 0.95 // 95% min success rate
};
this.currentWindow = this.createWindow();
}
/**
* Check if retry is allowed within budget
*/
canRetry(): boolean {
this.rotateWindowIfNeeded();
// Calculate current retry rate
const retryRate = this.currentWindow.totalRequests > 0
? this.currentWindow.totalRetries / this.currentWindow.totalRequests
: 0;
// Check if we're within budget
if (retryRate >= this.config.maxRetryRate) {
console.warn(
`[Budget] Retry budget exceeded: ${(retryRate * 100).toFixed(1)}% ` +
`(max: ${(this.config.maxRetryRate * 100).toFixed(1)}%)`
);
return false;
}
return true;
}
/**
* Record successful request
*/
recordSuccess(retriesUsed: number): void {
this.rotateWindowIfNeeded();
this.currentWindow.totalRequests++;
this.currentWindow.totalRetries += retriesUsed;
this.currentWindow.successfulRequests++;
}
/**
* Record failed request
*/
recordFailure(): void {
this.rotateWindowIfNeeded();
this.currentWindow.totalRequests++;
this.currentWindow.failedRequests++;
}
/**
* Get current budget statistics
*/
getStats(): {
retryRate: number;
successRate: number;
withinBudget: boolean;
} {
this.rotateWindowIfNeeded();
const retryRate = this.currentWindow.totalRequests > 0
? this.currentWindow.totalRetries / this.currentWindow.totalRequests
: 0;
const successRate = this.currentWindow.totalRequests > 0
? this.currentWindow.successfulRequests / this.currentWindow.totalRequests
: 1;
const withinBudget =
retryRate <= this.config.maxRetryRate &&
successRate >= this.config.minSuccessRate;
return { retryRate, successRate, withinBudget };
}
private createWindow(): BudgetWindow {
return {
startTime: Date.now(),
totalRequests: 0,
totalRetries: 0,
successfulRequests: 0,
failedRequests: 0
};
}
private rotateWindowIfNeeded(): void {
const elapsed = Date.now() - this.currentWindow.startTime;
if (elapsed >= this.config.windowMs) {
this.currentWindow = this.createWindow();
}
}
}
Explore our pricing plans to get production-grade retry infrastructure included.
Putting It All Together: Complete Integration
Here's how to integrate all five components into a production MCP server:
// mcp-server.ts
import { RetryHandler } from './retry-handler';
import { IdempotencyChecker } from './idempotency-checker';
import { DLQManager } from './dlq-manager';
const retryHandler = new RetryHandler({
maxRetries: 5,
baseDelayMs: 1000,
maxDelayMs: 32000
});
const idempotencyChecker = new IdempotencyChecker(86400000); // 24 hours
const dlqManager = new DLQManager(10000);
export async function callExternalAPI(
userId: string,
operation: string,
params: any
): Promise<any> {
// Generate idempotency key
const idempotencyKey = idempotencyChecker.generateKey(
userId,
operation,
params
);
// Check for duplicate request
const { isDuplicate, cachedResponse } = await idempotencyChecker.checkIdempotency(
idempotencyKey,
{ userId, operation, params }
);
if (isDuplicate) {
console.log('[API] Returning cached response');
return cachedResponse;
}
// Execute with retry logic
try {
const response = await retryHandler.executeWithRetry(
async () => {
// Your actual API call here
return await fetch('https://api.example.com/endpoint', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params)
}).then(res => res.json());
}
);
// Store in idempotency cache
await idempotencyChecker.storeResponse(
idempotencyKey,
{ userId, operation, params },
response
);
return response;
} catch (error) {
// Add to dead letter queue
await dlqManager.enqueue(
operation,
{ userId, params },
error as Error,
retryHandler['config'].maxRetries,
{ idempotencyKey }
);
throw error;
}
}
See live examples in our template marketplace with production-ready retry patterns.
Best Practices for ChatGPT App Retries
1. Implement Circuit Breakers
Prevent cascading failures by opening circuits when error rates exceed thresholds:
class CircuitBreaker {
private failures = 0;
private threshold = 5;
private state: 'closed' | 'open' | 'half-open' = 'closed';
async execute<T>(operation: () => Promise<T>): Promise<T> {
if (this.state === 'open') {
throw new Error('Circuit breaker open');
}
try {
const result = await operation();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
private onSuccess() {
this.failures = 0;
this.state = 'closed';
}
private onFailure() {
this.failures++;
if (this.failures >= this.threshold) {
this.state = 'open';
setTimeout(() => { this.state = 'half-open'; }, 60000);
}
}
}
2. Use Request Timeouts
Always set aggressive timeouts to prevent hanging requests:
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000);
try {
const response = await fetch(url, { signal: controller.signal });
return await response.json();
} finally {
clearTimeout(timeout);
}
3. Log Retry Metrics
Track retry patterns to identify systemic issues:
- Retry rate by operation type
- Average retries per successful request
- DLQ growth rate
- Retry budget health
Learn more about ChatGPT app monitoring for comprehensive observability.
4. Implement Graceful Degradation
Return partial results instead of complete failures:
try {
return await fetchFullData();
} catch (error) {
console.warn('Full data fetch failed, returning cached data');
return await fetchCachedData();
}
5. Respect Rate Limits Proactively
Don't wait for 429 errors—implement rate limiting on your side:
class RateLimiter {
private tokens: number;
private maxTokens = 100;
private refillRate = 10; // tokens per second
async acquire(): Promise<void> {
while (this.tokens < 1) {
await new Promise(resolve => setTimeout(resolve, 100));
}
this.tokens--;
}
}
Explore how MakeAIHQ handles rate limiting automatically in generated apps.
Common Retry Pitfalls to Avoid
- Retrying non-idempotent operations without idempotency keys: Always use idempotency for mutations
- Fixed backoff without jitter: Causes thundering herd problems
- Infinite retries: Always enforce max retry limits
- Retrying non-transient errors: Don't retry 400, 401, 403, 404 errors
- Ignoring retry budgets: Retry storms can cascade to upstream services
- Synchronous retries blocking other requests: Use async retry queues
- Not logging retry metrics: You can't optimize what you don't measure
Read our complete guide to ChatGPT app architecture for more production patterns.
Conclusion: Build Resilient ChatGPT Apps
Exponential backoff, idempotency, dead letter queues, and retry budgets are the foundation of production-grade ChatGPT apps. These patterns prevent cascading failures, improve user experience, and enable graceful degradation under load.
The code examples in this guide are production-ready—copy them directly into your MCP server for immediate resilience improvements.
Ready to build bulletproof ChatGPT apps without writing retry logic yourself?
Start building with MakeAIHQ's AI Editor—our platform generates production-ready MCP servers with retry strategies, idempotency, and error handling built in. From zero to ChatGPT App Store in 48 hours, no coding required.
Try MakeAIHQ free for 24 hours and deploy your first resilient ChatGPT app today.
Related Resources
- ChatGPT App Builder Complete Guide - Master ChatGPT app development
- Error Handling Patterns for ChatGPT Apps - Comprehensive error handling strategies
- Security Best Practices for ChatGPT Apps - Production security patterns
- Monitoring ChatGPT Apps in Production - Observability and metrics
- Rate Limiting Strategies for ChatGPT Apps - Prevent API throttling
- MakeAIHQ Features - See retry strategies in action
- Instant App Wizard - Deploy resilient apps in minutes
- AWS Exponential Backoff and Jitter - Industry best practices
- Google Cloud Retry Strategy Guide - Enterprise patterns
- Stripe Idempotent Requests - Real-world implementation
Questions? Contact our team for personalized ChatGPT app architecture guidance.