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 respondsapp.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
| Scenario | Typical Duration | Background? |
|---|---|---|
| AI image generation | 5–60s | Yes |
| PDF report generation | 10–120s | Yes |
| Payment processing | 2–10s | Depends |
| Sending transactional email | 1–3s | Yes |
| Third-party data enrichment | 5–30s | Yes |
| Database query | <1s | No |
As a rule of thumb: if it can timeout, it should be a background job.