logo

How to Handle Long-Running API Calls

Some API calls take a long time. AI model inference, PDF report generation, payment processing, fetching data from slow third-party services — these can range from seconds to minutes. Making these calls synchronously forces users to stare at a loading spinner while your server ties up a connection.

New to async processing? Check the glossary for key terms: task queue, webhook, callback, payload.

The Problem

// Your endpoint blocks until the slow API responds
app.post('/api/generate-report', async (req, res) => {
const data = await fetchAnalyticsData(req.body.dateRange); // 3-5 seconds
const report = await callReportingAPI(data); // 10-60 seconds
const pdf = await generatePDF(report); // 5-15 seconds
res.json({ reportUrl: pdf.url }); // User waited 18-80 seconds
});

This causes several problems:

  • Timeouts: Load balancers, API gateways, and browsers all have timeout limits (often 30 seconds)
  • Resource exhaustion: Each pending request holds a connection and memory
  • Poor UX: Users have no visibility into progress and may retry, compounding the load

The Solution

Step 1: Identify the long-running API call

Measure your endpoints. Any call that regularly exceeds a few seconds is a candidate for background processing. Common examples:

  • AI/ML inference: Image generation, LLM completions, video analysis
  • Report generation: Aggregating data and rendering PDFs or spreadsheets
  • Third-party APIs: Payment processing, shipping rate calculations, credit checks
  • Data pipelines: Extract, Transform, Load (ETL) jobs, bulk imports, cross-service data synchronization

Step 2: Offload the call to AsyncQueue

Replace the inline API call with a task and respond to the user immediately with a job ID:

app.post('/api/generate-report', async (req, res) => {
const jobId = crypto.randomUUID();
await saveToDatabase({
id: jobId,
type: 'report',
status: 'pending',
params: req.body,
});
await aq.tasks.create({
targetUrl: 'https://your-app.com/api/run-report',
payload: {
jobId,
dateRange: req.body.dateRange,
format: req.body.format || 'pdf',
},
webhookUrl: 'https://your-app.com/api/on-report-ready',
retries: 3,
timeout: 120, // seconds — generous limit for slow APIs
});
res.json({ jobId, status: 'pending' });
});

Your endpoint now responds in milliseconds, giving the user a job ID to track progress.

Step 3: Build a callback endpoint to execute the call

This endpoint does the heavy lifting. AsyncQueue calls it and waits for the result:

app.post('/api/run-report', async (req, res) => {
const { jobId, dateRange, format } = req.body;
await updateDatabase(jobId, { status: 'processing' });
const data = await fetchAnalyticsData(dateRange);
const report = await callReportingAPI(data);
const pdf = await generatePDF(report);
res.json({
jobId,
reportUrl: pdf.url,
pageCount: pdf.pages,
});
});

If the call fails, AsyncQueue automatically retries based on your configuration.

Step 4: Handle the result via webhook

When the task completes, AsyncQueue delivers the result to your webhook:

app.post('/api/on-report-ready', async (req, res) => {
const { jobId, reportUrl, pageCount } = req.body.result;
await updateDatabase(jobId, {
status: 'completed',
reportUrl,
pageCount,
});
await notifyUser(jobId, 'Your report is ready to download.');
res.status(200).json({ received: true });
});

Step 5: Add timeout handling and retries

Configure your tasks for the reality of unreliable external APIs:

await aq.tasks.create({
targetUrl: 'https://your-app.com/api/run-report',
payload: { jobId, dateRange },
webhookUrl: 'https://your-app.com/api/on-report-ready',
retries: 3, // retry up to 3 times on failure
timeout: 120, // allow up to 2 minutes per attempt
});

On the frontend, let users check status with polling:

const pollJobStatus = async (jobId) => {
const res = await fetch(`/api/jobs/${jobId}`);
const data = await res.json();
if (data.status === 'completed') {
showDownloadLink(data.reportUrl);
} else if (data.status === 'failed') {
showError('Report generation failed. Please try again.');
} else {
setTimeout(() => pollJobStatus(jobId), 3000);
}
};

When to Use This Pattern

ScenarioTypical DurationBackground?
AI image generation5–60sYes
PDF report generation10–120sYes
Payment processing2–10sDepends
Sending transactional email1–3sYes
Third-party data enrichment5–30sYes
Database query<1sNo

As a rule of thumb: if it can timeout, it should be a background job.