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({
callbackUrl: '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({
callbackUrl: '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.