How to Add API Status Monitoring to Your Next.js App
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 Stack — Better 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
- Better UX: Users see "Stripe is experiencing issues" instead of "Payment failed - please try again"
- Reduced Support Load: Status badges prevent users from submitting "Is it just me?" tickets
- Faster Debugging: Know immediately whether failures are your code or external services
- 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
- Check before you call: Always verify API status before making critical requests
- Cache aggressively: Status checks should be fast—cache for 1-2 minutes
- Fail gracefully: Never show raw errors to users; provide context and alternatives
- Monitor everything: Track both your dependencies and your status checks
- 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
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.”
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.”
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.”
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.”
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.”
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.
Free dashboard available · 14-day trial on paid plans · Cancel anytime
Browse Free Dashboard →