How to Add API Status Monitoring to Your Next.js App

by Shibley Rahman

How to Add API Status Monitoring to Your Next.js App

If your Next.js app depends on third-party APIs like Stripe, OpenAI, or Twilio, you've probably experienced the frustration of debugging issues only to discover the external service was down. Your users don't care whose fault it is—they just know your app isn't working.

In this guide, you'll learn how to integrate real-time API status monitoring into your Next.js application, so you can:

  • Proactively inform users when third-party services are experiencing issues
  • Reduce support tickets by displaying status information before users encounter errors
  • Handle outages gracefully with fallback behavior and clear messaging
  • Get notified immediately when APIs your app depends on go down

We'll use the API Status Check public API for examples, but the patterns apply to any status monitoring service.

Why Monitor Third-Party API Status in Your App?

📡 Add real-time monitoring to your Next.js app with Better StackBetter Stack monitors your endpoints every 30 seconds and alerts you instantly via Slack, email, or SMS.

The Problem: Silent Failures

Your production Next.js app is humming along, then suddenly:

// Your perfectly good code starts failing
const response = await stripe.customers.create({
  email: user.email,
});
// Error: connect ETIMEDOUT

Your error monitoring lights up. Support tickets flood in. You spend 30 minutes debugging before checking Twitter and discovering Stripe is having an outage.

The Solution: Proactive Status Awareness

Instead of reactive debugging, integrate status monitoring directly into your app:

// Check status before attempting the operation
const stripeStatus = await checkAPIStatus('stripe');

if (stripeStatus.status !== 'operational') {
  // Show user a clear message instead of cryptic errors
  return {
    error: 'Payment processing is temporarily unavailable. Please try again in a few minutes.',
    canRetry: true
  };
}

Real-World Benefits

  1. Better UX: Users see "Stripe is experiencing issues" instead of "Payment failed - please try again"
  2. Reduced Support Load: Status badges prevent users from submitting "Is it just me?" tickets
  3. Faster Debugging: Know immediately whether failures are your code or external services
  4. Increased Trust: Transparency about third-party issues shows you're on top of things

Using the API Status Check Public API

Let's build a robust API status monitoring system for your Next.js app.

1. Create a Status Service

First, create a reusable service to fetch API status data:

// lib/api-status.ts
import { unstable_cache } from 'next/cache';

export type APIStatus = 'operational' | 'degraded' | 'partial_outage' | 'major_outage' | 'maintenance';

export interface StatusResponse {
  service: string;
  status: APIStatus;
  lastChecked: string;
  incidents: Array<{
    title: string;
    status: string;
    impact: string;
    createdAt: string;
  }>;
}

export interface StatusCheckResult {
  service: string;
  status: APIStatus;
  isOperational: boolean;
  message?: string;
  lastChecked: Date;
}

/**
 * Fetch status for a specific API service
 * Results are cached for 2 minutes to avoid excessive API calls
 */
export const getAPIStatus = unstable_cache(
  async (serviceName: string): Promise<StatusCheckResult> => {
    try {
      const response = await fetch(
        `https://api.apistatuscheck.com/v1/status/${serviceName.toLowerCase()}`,
        {
          headers: {
            'Accept': 'application/json',
          },
          next: { revalidate: 120 } // Cache for 2 minutes
        }
      );

      if (!response.ok) {
        throw new Error(`Status API returned ${response.status}`);
      }

      const data: StatusResponse = await response.json();

      return {
        service: data.service,
        status: data.status,
        isOperational: data.status === 'operational',
        message: data.incidents[0]?.title,
        lastChecked: new Date(data.lastChecked),
      };
    } catch (error) {
      console.error(`Failed to fetch status for ${serviceName}:`, error);
      
      // Default to operational if status check fails
      // Don't block your app if the monitoring service is down
      return {
        service: serviceName,
        status: 'operational',
        isOperational: true,
        message: 'Status check unavailable',
        lastChecked: new Date(),
      };
    }
  },
  ['api-status'], // Cache key
  { revalidate: 120 } // Revalidate every 2 minutes
);

/**
 * Check multiple services at once
 */
export async function checkMultipleServices(
  services: string[]
): Promise<Record<string, StatusCheckResult>> {
  const results = await Promise.allSettled(
    services.map(service => getAPIStatus(service))
  );

  return services.reduce((acc, service, index) => {
    const result = results[index];
    acc[service] = result.status === 'fulfilled' 
      ? result.value 
      : {
          service,
          status: 'operational' as APIStatus,
          isOperational: true,
          message: 'Status check failed',
          lastChecked: new Date(),
        };
    return acc;
  }, {} as Record<string, StatusCheckResult>);
}

2. Server Component Integration

Use React Server Components to fetch status data without client-side overhead:

// app/dashboard/page.tsx
import { getAPIStatus, checkMultipleServices } from '@/lib/api-status';
import { StatusBadge } from '@/components/status-badge';

export default async function DashboardPage() {
  // Check all critical services your app depends on
  const services = ['stripe', 'openai', 'twilio', 'sendgrid'];
  const statuses = await checkMultipleServices(services);

  return (
    <div className="container mx-auto p-6">
      <h1 className="text-3xl font-bold mb-6">Dashboard</h1>
      
      {/* Show status of critical dependencies */}
      <div className="bg-white rounded-lg shadow p-6 mb-6">
        <h2 className="text-xl font-semibold mb-4">Service Status</h2>
        <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
          {services.map(service => (
            <StatusBadge
              key={service}
              service={service}
              status={statuses[service].status}
              message={statuses[service].message}
            />
          ))}
        </div>
      </div>

      {/* Rest of your dashboard */}
    </div>
  );
}

3. API Route for Client-Side Polling

For real-time updates, create an API route that clients can poll:

// app/api/status/[service]/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { getAPIStatus } from '@/lib/api-status';

export async function GET(
  request: NextRequest,
  { params }: { params: { service: string } }
) {
  const { service } = params;

  if (!service) {
    return NextResponse.json(
      { error: 'Service name required' },
      { status: 400 }
    );
  }

  const status = await getAPIStatus(service);

  return NextResponse.json(status, {
    headers: {
      'Cache-Control': 'public, s-maxage=120, stale-while-revalidate=300',
    },
  });
}

// Check multiple services at once
// GET /api/status?services=stripe,openai,twilio
export async function GET(request: NextRequest) {
  const searchParams = request.nextUrl.searchParams;
  const servicesParam = searchParams.get('services');

  if (!servicesParam) {
    return NextResponse.json(
      { error: 'Services parameter required' },
      { status: 400 }
    );
  }

  const services = servicesParam.split(',').map(s => s.trim());
  const statuses = await checkMultipleServices(services);

  return NextResponse.json(statuses, {
    headers: {
      'Cache-Control': 'public, s-maxage=120, stale-while-revalidate=300',
    },
  });
}

Displaying Status Badges in Your UI

Now let's create beautiful, informative status indicators.

Basic Status Badge Component

// components/status-badge.tsx
'use client';

import { APIStatus } from '@/lib/api-status';
import { 
  CheckCircle2, 
  AlertCircle, 
  AlertTriangle, 
  XCircle,
  Wrench 
} from 'lucide-react';

interface StatusBadgeProps {
  service: string;
  status: APIStatus;
  message?: string;
  showLabel?: boolean;
  size?: 'sm' | 'md' | 'lg';
}

export function StatusBadge({
  service,
  status,
  message,
  showLabel = true,
  size = 'md',
}: StatusBadgeProps) {
  const config = {
    operational: {
      label: 'Operational',
      color: 'text-green-600 bg-green-50 border-green-200',
      icon: CheckCircle2,
    },
    degraded: {
      label: 'Degraded',
      color: 'text-yellow-600 bg-yellow-50 border-yellow-200',
      icon: AlertTriangle,
    },
    partial_outage: {
      label: 'Partial Outage',
      color: 'text-orange-600 bg-orange-50 border-orange-200',
      icon: AlertCircle,
    },
    major_outage: {
      label: 'Major Outage',
      color: 'text-red-600 bg-red-50 border-red-200',
      icon: XCircle,
    },
    maintenance: {
      label: 'Maintenance',
      color: 'text-blue-600 bg-blue-50 border-blue-200',
      icon: Wrench,
    },
  };

  const { label, color, icon: Icon } = config[status];

  const sizeClasses = {
    sm: 'text-xs px-2 py-1',
    md: 'text-sm px-3 py-1.5',
    lg: 'text-base px-4 py-2',
  };

  return (
    <div className="flex flex-col gap-1">
      <div className={`inline-flex items-center gap-2 rounded-md border ${color} ${sizeClasses[size]} font-medium`}>
        <Icon className="h-4 w-4" />
        <span className="capitalize">{service}</span>
        {showLabel && (
          <>
            <span className="text-gray-400">•</span>
            <span>{label}</span>
          </>
        )}
      </div>
      {message && (
        <p className="text-xs text-gray-600 ml-1">{message}</p>
      )}
    </div>
  );
}

Real-Time Status Dashboard Component

A client component that polls for updates:

// components/real-time-status-dashboard.tsx
'use client';

import { useState, useEffect } from 'react';
import { StatusBadge } from './status-badge';
import { APIStatus, StatusCheckResult } from '@/lib/api-status';

interface RealTimeStatusDashboardProps {
  services: string[];
  pollInterval?: number; // milliseconds
}

export function RealTimeStatusDashboard({
  services,
  pollInterval = 60000, // Default: check every minute
}: RealTimeStatusDashboardProps) {
  const [statuses, setStatuses] = useState<Record<string, StatusCheckResult>>({});
  const [loading, setLoading] = useState(true);
  const [lastUpdate, setLastUpdate] = useState<Date>(new Date());

  useEffect(() => {
    const fetchStatuses = async () => {
      try {
        const response = await fetch(
          `/api/status?services=${services.join(',')}`
        );
        const data = await response.json();
        setStatuses(data);
        setLastUpdate(new Date());
      } catch (error) {
        console.error('Failed to fetch statuses:', error);
      } finally {
        setLoading(false);
      }
    };

    // Fetch immediately
    fetchStatuses();

    // Then poll at interval
    const interval = setInterval(fetchStatuses, pollInterval);

    return () => clearInterval(interval);
  }, [services, pollInterval]);

  if (loading) {
    return (
      <div className="animate-pulse">
        <div className="h-24 bg-gray-200 rounded"></div>
      </div>
    );
  }

  const hasIssues = Object.values(statuses).some(s => !s.isOperational);

  return (
    <div className="bg-white rounded-lg shadow-lg p-6">
      <div className="flex items-center justify-between mb-4">
        <h2 className="text-xl font-semibold">Service Status</h2>
        <div className="text-sm text-gray-500">
          Last updated: {lastUpdate.toLocaleTimeString()}
        </div>
      </div>

      {hasIssues && (
        <div className="mb-4 p-4 bg-yellow-50 border border-yellow-200 rounded-lg">
          <p className="text-sm text-yellow-800">
            ⚠️ One or more services are experiencing issues. Some features may be unavailable.
          </p>
        </div>
      )}

      <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
        {services.map(service => {
          const status = statuses[service];
          if (!status) return null;

          return (
            <StatusBadge
              key={service}
              service={status.service}
              status={status.status}
              message={status.message}
              size="lg"
            />
          );
        })}
      </div>
    </div>
  );
}

Inline Status Indicator

Show status inline with features that depend on external APIs:

// components/payment-form.tsx
'use client';

import { useState, useEffect } from 'react';
import { getAPIStatus } from '@/lib/api-status';

export function PaymentForm() {
  const [stripeStatus, setStripeStatus] = useState<'operational' | 'down'>('operational');
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    // Check Stripe status on mount
    fetch('/api/status/stripe')
      .then(res => res.json())
      .then(data => {
        setStripeStatus(data.isOperational ? 'operational' : 'down');
      });
  }, []);

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();

    if (stripeStatus === 'down') {
      alert('Payment processing is currently unavailable. Please try again later.');
      return;
    }

    setLoading(true);
    // Process payment...
  };

  return (
    <form onSubmit={handleSubmit} className="space-y-4">
      <h2 className="text-2xl font-bold">Complete Payment</h2>

      {stripeStatus === 'down' && (
        <div className="p-4 bg-red-50 border border-red-200 rounded-lg">
          <p className="text-sm text-red-800">
            ⚠️ Payment processing is currently experiencing issues. 
            Please wait a few minutes before trying again.
          </p>
        </div>
      )}

      {/* Payment form fields */}
      <input
        type="text"
        placeholder="Card number"
        className="w-full p-2 border rounded"
        disabled={stripeStatus === 'down'}
      />

      <button
        type="submit"
        disabled={loading || stripeStatus === 'down'}
        className="w-full bg-blue-600 text-white py-2 rounded disabled:bg-gray-400"
      >
        {loading ? 'Processing...' : 'Pay Now'}
      </button>
    </form>
  );
}

Setting Up Webhook Notifications for Outages

Get notified the moment an API you depend on goes down.

1. Create a Webhook Endpoint

// app/api/webhooks/status-alert/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { headers } from 'next/headers';

// Types for webhook payload
interface StatusWebhookPayload {
  service: string;
  status: 'operational' | 'degraded' | 'partial_outage' | 'major_outage';
  previousStatus: string;
  incident?: {
    title: string;
    description: string;
    impact: string;
    startedAt: string;
  };
  timestamp: string;
}

export async function POST(request: NextRequest) {
  // Verify webhook signature
  const headersList = headers();
  const signature = headersList.get('x-webhook-signature');
  const webhookSecret = process.env.API_STATUS_WEBHOOK_SECRET;

  if (!signature || !webhookSecret) {
    return NextResponse.json(
      { error: 'Unauthorized' },
      { status: 401 }
    );
  }

  // Verify signature (implementation depends on the service)
  const payload: StatusWebhookPayload = await request.json();
  
  // Handle different status changes
  switch (payload.status) {
    case 'major_outage':
    case 'partial_outage':
      await handleOutageAlert(payload);
      break;
    
    case 'degraded':
      await handleDegradationAlert(payload);
      break;
    
    case 'operational':
      if (payload.previousStatus !== 'operational') {
        await handleResolutionAlert(payload);
      }
      break;
  }

  return NextResponse.json({ received: true });
}

async function handleOutageAlert(payload: StatusWebhookPayload) {
  // Send alerts to your team
  await Promise.all([
    // Send Slack notification
    sendSlackAlert({
      channel: '#engineering-alerts',
      text: `🚨 *${payload.service} is experiencing a ${payload.status}*\n${payload.incident?.title}`,
      priority: 'high',
    }),

    // Send email to on-call engineer
    sendEmailAlert({
      to: process.env.ONCALL_EMAIL!,
      subject: `URGENT: ${payload.service} outage detected`,
      body: `Service: ${payload.service}\nStatus: ${payload.status}\nIncident: ${payload.incident?.title}\n\nStarted at: ${payload.incident?.startedAt}`,
    }),

    // Update status page (if you have one)
    updateInternalStatusPage({
      service: payload.service,
      status: payload.status,
      message: payload.incident?.title,
    }),
  ]);

  // Log to monitoring
  console.error('API outage detected:', {
    service: payload.service,
    status: payload.status,
    incident: payload.incident,
  });
}

async function handleDegradationAlert(payload: StatusWebhookPayload) {
  // Less urgent - just notify via Slack
  await sendSlackAlert({
    channel: '#engineering',
    text: `⚠️ ${payload.service} is experiencing degraded performance\n${payload.incident?.title}`,
    priority: 'medium',
  });
}

async function handleResolutionAlert(payload: StatusWebhookPayload) {
  // Good news - service is back
  await sendSlackAlert({
    channel: '#engineering-alerts',
    text: `✅ ${payload.service} has returned to operational status`,
    priority: 'low',
  });
}

// Helper functions
async function sendSlackAlert(params: {
  channel: string;
  text: string;
  priority: 'high' | 'medium' | 'low';
}) {
  if (!process.env.SLACK_WEBHOOK_URL) return;

  await fetch(process.env.SLACK_WEBHOOK_URL, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      channel: params.channel,
      text: params.text,
      username: 'API Status Monitor',
      icon_emoji: ':warning:',
    }),
  });
}

async function sendEmailAlert(params: {
  to: string;
  subject: string;
  body: string;
}) {
  // Use your email service (SendGrid, Resend, etc.)
  // Example with fetch to your email API
  await fetch('/api/send-email', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(params),
  });
}

async function updateInternalStatusPage(params: {
  service: string;
  status: string;
  message?: string;
}) {
  // Update your own status page or internal dashboard
  // This could write to a database, update a Redis cache, etc.
}

2. Configure Webhook Subscriptions

Register your webhook endpoint with API Status Check:

// scripts/setup-webhooks.ts
async function setupWebhooks() {
  const webhookUrl = `${process.env.NEXT_PUBLIC_APP_URL}/api/webhooks/status-alert`;
  
  // Services you want to monitor
  const services = ['stripe', 'openai', 'twilio', 'sendgrid'];

  for (const service of services) {
    const response = await fetch('https://api.apistatuscheck.com/v1/webhooks', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${process.env.API_STATUS_API_KEY}`,
      },
      body: JSON.stringify({
        url: webhookUrl,
        service: service,
        events: ['status_changed', 'incident_created', 'incident_resolved'],
        secret: process.env.API_STATUS_WEBHOOK_SECRET,
      }),
    });


> 🔐 **Manage your API keys securely when integrating status checks into Next.js** — [1Password](https://1password.partnerlinks.io/6t8opdyq764m?utm_source=asc&utm_medium=affiliate&utm_campaign=1password&utm_content=nextjs-api-status-integration) securely manages API keys, tokens, and credentials with automatic rotation when breaches occur.
    if (response.ok) {
      console.log(`✅ Webhook configured for ${service}`);
    } else {
      console.error(`❌ Failed to configure webhook for ${service}`);
    }
  }
}

setupWebhooks();

Best Practices for Graceful Degradation

When APIs go down, your app shouldn't just crash. Here's how to handle failures elegantly.

1. Circuit Breaker Pattern

Prevent cascading failures by stopping requests to services that are down:

// lib/circuit-breaker.ts
interface CircuitBreakerOptions {
  failureThreshold: number;
  resetTimeout: number; // milliseconds
}

class CircuitBreaker {
  private failures = 0;
  private lastFailureTime: number | null = null;
  private state: 'closed' | 'open' | 'half-open' = 'closed';

  constructor(private options: CircuitBreakerOptions) {}

  async execute<T>(fn: () => Promise<T>): Promise<T> {
    if (this.state === 'open') {
      // Check if enough time has passed to try again
      if (
        this.lastFailureTime &&
        Date.now() - this.lastFailureTime > this.options.resetTimeout
      ) {
        this.state = 'half-open';
      } else {
        throw new Error('Circuit breaker is open - service unavailable');
      }
    }

    try {
      const result = await fn();
      this.onSuccess();
      return result;
    } catch (error) {
      this.onFailure();
      throw error;
    }
  }

  private onSuccess() {
    this.failures = 0;
    this.state = 'closed';
  }

  private onFailure() {
    this.failures++;
    this.lastFailureTime = Date.now();

    if (this.failures >= this.options.failureThreshold) {
      this.state = 'open';
    }
  }

  getState() {
    return this.state;
  }
}

// Create circuit breakers for each external service
const breakers = {
  stripe: new CircuitBreaker({ failureThreshold: 3, resetTimeout: 60000 }),
  openai: new CircuitBreaker({ failureThreshold: 3, resetTimeout: 60000 }),
  twilio: new CircuitBreaker({ failureThreshold: 3, resetTimeout: 60000 }),
};

export { breakers, CircuitBreaker };

2. Fallback Strategies

Provide alternatives when primary services fail:

// lib/ai-service.ts
import { breakers } from './circuit-breaker';
import { getAPIStatus } from './api-status';

export async function generateText(prompt: string): Promise<string> {
  // Check if OpenAI is down
  const status = await getAPIStatus('openai');
  
  if (!status.isOperational) {
    // Fall back to local model or cached responses
    return fallbackTextGeneration(prompt);
  }

  try {
    return await breakers.openai.execute(async () => {
      const response = await fetch('https://api.openai.com/v1/chat/completions', {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${process.env.OPENAI_API_KEY}`,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          model: 'gpt-4',
          messages: [{ role: 'user', content: prompt }],
        }),
      });

      if (!response.ok) throw new Error('OpenAI request failed');

      const data = await response.json();
      return data.choices[0].message.content;
    });
  } catch (error) {
    console.error('OpenAI request failed, using fallback:', error);
    return fallbackTextGeneration(prompt);
  }
}

function fallbackTextGeneration(prompt: string): string {
  // Return a generic response or use a local model
  return "I apologize, but our AI service is temporarily unavailable. Please try again in a few minutes.";
}

3. Retry Logic with Exponential Backoff

Don't hammer failing services—back off gracefully:

// lib/retry.ts
interface RetryOptions {
  maxAttempts: number;
  initialDelay: number;
  maxDelay: number;
  backoffMultiplier: number;
}

export async function retryWithBackoff<T>(
  fn: () => Promise<T>,
  options: RetryOptions = {
    maxAttempts: 3,
    initialDelay: 1000,
    maxDelay: 10000,
    backoffMultiplier: 2,
  }
): Promise<T> {
  let lastError: Error;
  let delay = options.initialDelay;

  for (let attempt = 1; attempt <= options.maxAttempts; attempt++) {
    try {
      return await fn();
    } catch (error) {
      lastError = error as Error;

      if (attempt === options.maxAttempts) {
        throw lastError;
      }

      // Wait before retrying
      await new Promise(resolve => setTimeout(resolve, delay));

      // Increase delay for next attempt
      delay = Math.min(delay * options.backoffMultiplier, options.maxDelay);

      console.log(`Retry attempt ${attempt} failed, waiting ${delay}ms before next attempt`);
    }
  }

  throw lastError!;
}

// Usage
const data = await retryWithBackoff(
  () => fetch('https://api.stripe.com/v1/customers').then(r => r.json()),
  { maxAttempts: 3, initialDelay: 1000, maxDelay: 5000, backoffMultiplier: 2 }
);

4. Queue Failed Requests

When services are down, queue operations for later:

// lib/request-queue.ts
import { Redis } from '@upstash/redis';

const redis = new Redis({
  url: process.env.UPSTASH_REDIS_URL!,
  token: process.env.UPSTASH_REDIS_TOKEN!,
});

interface QueuedRequest {
  id: string;
  service: string;
  endpoint: string;
  payload: any;
  timestamp: number;
  retries: number;
}

export async function queueFailedRequest(
  service: string,
  endpoint: string,
  payload: any
) {
  const request: QueuedRequest = {
    id: crypto.randomUUID(),
    service,
    endpoint,
    payload,
    timestamp: Date.now(),
    retries: 0,
  };

  await redis.lpush(`queue:${service}`, JSON.stringify(request));
  console.log(`Queued request for ${service}: ${request.id}`);
}

export async function processQueue(service: string) {
  // Check if service is back online
  const status = await getAPIStatus(service);
  if (!status.isOperational) {
    console.log(`${service} still down, skipping queue processing`);
    return;
  }

  // Process queued requests
  let processed = 0;
  while (true) {
    const item = await redis.rpop(`queue:${service}`);
    if (!item) break;

    const request: QueuedRequest = JSON.parse(item);

    try {
      // Retry the request
      await fetch(request.endpoint, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(request.payload),
      });

      processed++;
      console.log(`Successfully processed queued request ${request.id}`);
    } catch (error) {
      // Re-queue if still failing
      if (request.retries < 3) {
        request.retries++;
        await redis.lpush(`queue:${service}`, JSON.stringify(request));
      } else {
        console.error(`Abandoned request ${request.id} after 3 retries`);
      }
    }
  }

  console.log(`Processed ${processed} queued requests for ${service}`);
}

5. User-Facing Error Messages

Show helpful messages instead of technical errors:

// lib/error-messages.ts
import { APIStatus } from './api-status';

export function getErrorMessage(service: string, status: APIStatus): string {
  const messages = {
    operational: '',
    degraded: `${service} is currently slow. Your request may take longer than usual.`,
    partial_outage: `Some ${service} features are unavailable. We're working on it!`,
    major_outage: `${service} is temporarily unavailable. Please try again in a few minutes.`,
    maintenance: `${service} is under scheduled maintenance. Service will resume shortly.`,
  };

  return messages[status] || `${service} is experiencing issues.`;
}

// Usage in your API routes
export async function POST(request: NextRequest) {
  const stripeStatus = await getAPIStatus('stripe');

  if (!stripeStatus.isOperational) {
    return NextResponse.json(
      {
        error: getErrorMessage('Stripe', stripeStatus.status),
        canRetry: stripeStatus.status !== 'major_outage',
        estimatedRecovery: '5-10 minutes',
      },
      { status: 503 }
    );
  }

  // Process normally...
}

Complete Working Example: Payment Processing with Status Checks

Here's a full implementation showing all patterns together:

// app/api/payments/create/route.ts
import { NextRequest, NextResponse } from 'next/server';
import Stripe from 'stripe';
import { getAPIStatus } from '@/lib/api-status';
import { breakers } from '@/lib/circuit-breaker';
import { retryWithBackoff } from '@/lib/retry';
import { queueFailedRequest } from '@/lib/request-queue';
import { getErrorMessage } from '@/lib/error-messages';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  apiVersion: '2023-10-16',
});

export async function POST(request: NextRequest) {
  const { amount, currency, customerId } = await request.json();

  // 1. Check Stripe status before attempting
  const stripeStatus = await getAPIStatus('stripe');

  if (stripeStatus.status === 'major_outage') {
    // Queue for later processing
    await queueFailedRequest('stripe', '/api/payments/create', {
      amount,
      currency,
      customerId,
    });

    return NextResponse.json(
      {
        error: getErrorMessage('Stripe', stripeStatus.status),
        queued: true,
        message: 'Your payment will be processed when service is restored.',
      },
      { status: 503 }
    );
  }

  if (!stripeStatus.isOperational) {
    // Degraded or partial outage - warn but allow
    console.warn(`Stripe status: ${stripeStatus.status}`);
  }

  // 2. Use circuit breaker to prevent cascading failures
  try {
    const paymentIntent = await breakers.stripe.execute(async () => {
      // 3. Retry with exponential backoff
      return await retryWithBackoff(
        async () => {
          return await stripe.paymentIntents.create({
            amount,
            currency,
            customer: customerId,
            metadata: {
              createdAt: new Date().toISOString(),
              statusCheck: stripeStatus.status,
            },
          });
        },
        { maxAttempts: 3, initialDelay: 1000, maxDelay: 5000, backoffMultiplier: 2 }
      );
    });

    return NextResponse.json({
      success: true,
      clientSecret: paymentIntent.client_secret,
      warning: !stripeStatus.isOperational
        ? getErrorMessage('Stripe', stripeStatus.status)
        : undefined,
    });
  } catch (error) {
    console.error('Payment creation failed:', error);

    // Check if circuit breaker is open
    if (breakers.stripe.getState() === 'open') {
      // Queue for later
      await queueFailedRequest('stripe', '/api/payments/create', {
        amount,
        currency,
        customerId,
      });

      return NextResponse.json(
        {
          error: 'Payment service is temporarily unavailable',
          queued: true,
          canRetry: true,
        },
        { status: 503 }
      );
    }

    // Regular failure
    return NextResponse.json(
      {
        error: 'Payment failed. Please try again.',
        canRetry: true,
      },
      { status: 500 }
    );
  }
}

Performance Considerations

Caching Strategy

// Use Next.js caching effectively
export const revalidate = 120; // Revalidate every 2 minutes

// Or use Redis for distributed caching
import { Redis } from '@upstash/redis';

const redis = new Redis({
  url: process.env.UPSTASH_REDIS_URL!,
  token: process.env.UPSTASH_REDIS_TOKEN!,
});

export async function getCachedStatus(service: string) {
  const cached = await redis.get(`status:${service}`);
  
  if (cached) {
    return JSON.parse(cached as string);
  }

  const status = await getAPIStatus(service);
  await redis.set(`status:${service}`, JSON.stringify(status), {
    ex: 120, // Expire after 2 minutes
  });

  return status;
}

Parallel Checks

// Check multiple services in parallel, not sequentially
const [stripeStatus, openaiStatus, twilioStatus] = await Promise.all([
  getAPIStatus('stripe'),
  getAPIStatus('openai'),
  getAPIStatus('twilio'),
]);

Background Status Updates

// app/api/cron/update-statuses/route.ts
import { NextResponse } from 'next/server';
import { checkMultipleServices } from '@/lib/api-status';
import { Redis } from '@upstash/redis';

const redis = new Redis({
  url: process.env.UPSTASH_REDIS_URL!,
  token: process.env.UPSTASH_REDIS_TOKEN!,
});

// Vercel Cron or similar
export async function GET() {
  const services = ['stripe', 'openai', 'twilio', 'sendgrid'];
  const statuses = await checkMultipleServices(services);

  // Update cache
  await Promise.all(
    Object.entries(statuses).map(([service, status]) =>
      redis.set(`status:${service}`, JSON.stringify(status), { ex: 300 })
    )
  );

  return NextResponse.json({ updated: Object.keys(statuses) });
}

Monitoring Your Monitoring

Don't forget to monitor your status checks themselves:

// lib/observability.ts
export async function trackStatusCheck(
  service: string,
  duration: number,
  success: boolean
) {
  // Send to your analytics/monitoring
  await fetch('https://api.your-analytics.com/events', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      event: 'status_check',
      service,
      duration,
      success,
      timestamp: Date.now(),
    }),
  });
}

// Usage
const start = Date.now();
try {
  const status = await getAPIStatus('stripe');
  await trackStatusCheck('stripe', Date.now() - start, true);
  return status;
} catch (error) {
  await trackStatusCheck('stripe', Date.now() - start, false);
  throw error;
}

Summary

You now have a complete API status monitoring system for your Next.js app:

Real-time status checks with caching and performance optimization
Beautiful UI components for displaying status to users
Webhook notifications to alert your team immediately
Circuit breakers to prevent cascading failures
Retry logic with exponential backoff
Request queuing for handling prolonged outages
User-friendly error messages that maintain trust

Key Takeaways

  1. Check before you call: Always verify API status before making critical requests
  2. Cache aggressively: Status checks should be fast—cache for 1-2 minutes
  3. Fail gracefully: Never show raw errors to users; provide context and alternatives
  4. Monitor everything: Track both your dependencies and your status checks
  5. Communicate proactively: Show status badges so users know what's happening

Next Steps

  • Set up a status page for your own app using this data
  • Implement automated failover to backup services
  • Create dashboards showing historical uptime data
  • Build alerting rules based on status patterns

Have questions? Check out the API Status Check documentation or join our Discord community.


Looking for a hosted solution? API Status Check monitors 500+ APIs and provides instant notifications when services go down. Get started free.

🛠 Tools We Use & Recommend

Tested across our own infrastructure monitoring 200+ APIs daily

Better StackBest for API Teams

Uptime Monitoring & Incident Management

Used by 100,000+ websites

Monitors your APIs every 30 seconds. Instant alerts via Slack, email, SMS, and phone calls when something goes down.

We use Better Stack to monitor every API on this site. It caught 23 outages last month before users reported them.

Free tier · Paid from $24/moStart Free Monitoring
1PasswordBest for Credential Security

Secrets Management & Developer Security

Trusted by 150,000+ businesses

Manage API keys, database passwords, and service tokens with CLI integration and automatic rotation.

After covering dozens of outages caused by leaked credentials, we recommend every team use a secrets manager.

OpteryBest for Privacy

Automated Personal Data Removal

Removes data from 350+ brokers

Removes your personal data from 350+ data broker sites. Protects against phishing and social engineering attacks.

Service outages sometimes involve data breaches. Optery keeps your personal info off the sites attackers use first.

From $9.99/moFree Privacy Scan
ElevenLabsBest for AI Voice

AI Voice & Audio Generation

Used by 1M+ developers

Text-to-speech, voice cloning, and audio AI for developers. Build voice features into your apps with a simple API.

The best AI voice API we've tested — natural-sounding speech with low latency. Essential for any app adding voice features.

Free tier · Paid from $5/moTry ElevenLabs Free
SEMrushBest for SEO

SEO & Site Performance Monitoring

Used by 10M+ marketers

Track your site health, uptime, search rankings, and competitor movements from one dashboard.

We use SEMrush to track how our API status pages rank and catch site health issues early.

From $129.95/moTry SEMrush Free
View full comparison & more tools →Affiliate links — we earn a commission at no extra cost to you

API Status Check

Stop checking API status pages manually

Get instant email alerts when OpenAI, Stripe, AWS, and 100+ APIs go down. Know before your users do.

Get Alerts — $9/mo →

Free dashboard available · 14-day trial on paid plans · Cancel anytime

Browse Free Dashboard →