Handling Long-Running AI Tasks in Next.js: Upstash QStash, Inngest, and Serverless Timeouts
When LLM completions, scraping, or PDF generation take 45+ seconds, Vercel serverless functions crash. Here is how to architect reliable asynchronous queues without running expensive servers.
Building an AI SaaS prototype on your local machine is deceptive. You send a prompt to OpenAI or Claude, wait 35 seconds, receive the generated report, and render it on your screen. Everything works.
Then you deploy your Next.js application to production on Vercel or AWS Amplify.
A customer uploads a 40-page financial document or clicks "Generate 10-Year DCF Analysis". The loading spinner turns for 15 seconds. Suddenly, the screen goes red:
504 GATEWAY TIMEOUT: FUNCTION_INVOCATION_TIMEOUT
Welcome to the serverless timeout wall.
On standard Vercel plans, serverless functions timeout after 15 seconds (or 60 seconds on Pro). When orchestrating complex AI pipelines—such as web scraping, multi-step LLM chaining, document OCR, or compiling stock valuation metrics—tasks frequently take 30 to 120 seconds.
Here is how we solved this exact challenge when engineering briefstock.ai and feedalyze.net, and how you can architect resilient, asynchronous AI workflows without spinning up expensive dedicated servers.
The Synchronous Anti-Pattern vs. The Asynchronous Queue
Most junior developers try to keep the client HTTP connection open while waiting for the LLM:
❌ The Synchronous Trap (Crashes on Timeout):
[ User Browser ] ───(HTTP POST /api/generate)───► [ Next.js API Route ] ───► [ OpenAI / Claude ]
(Hangs waiting...) (Killed at 15s/60s 💥) (Takes 45s)
Instead, production applications decouple the task submission from the task execution:
✅ The Asynchronous Job Queue Pattern:
1. [ Browser ] ──(POST /api/jobs)──► [ Next.js API ] ──► [ DB: status='queued' ]
│
▼
[ Serverless Queue ] (Upstash QStash / Inngest)
│
▼ (HTTP Webhook / Worker)
[ AI Worker Function ] ──► [ OpenAI / Claude ]
│
▼
[ DB: status='completed', result={...} ]
2. [ Browser ] ◄──(Polls / SSE / Supabase Realtime)── [ Updated State ]
When the user clicks "Generate", the frontend receives an immediate 202 Accepted response with a jobId in 150 milliseconds. The heavy processing happens in the background, and the UI updates reactively once the job finishes.
Evaluating the Top 3 Serverless Queues for 2026
You do not need to configure RabbitMQ, Celery, or a dedicated Redis VPS to run background jobs. Modern serverless queues trigger HTTP webhooks directly against your Next.js API routes.
1. Upstash QStash (Best for Simplicity & Micro-Budgets)
- How it works: A pure HTTP-to-HTTP messaging queue. You make a single HTTP POST request to QStash with your destination URL (
/api/workers/generate-report) and payload. QStash queues it, retries on failure with exponential backoff, and posts the payload to your endpoint. - Pros: Zero infrastructure, pay-per-request pricing ($0.40 per 100,000 messages), built-in rate-limiting, and deduplication.
- Cons: No complex multi-step state machine logic built-in.
2. Inngest (Best for Multi-Step AI Pipelines)
- How it works: Code-first durable execution engine. You write background workflows using
step.run()andstep.sleep()directly in TypeScript. - Pros: Exceptional developer experience. If step 2 (AI summary) succeeds but step 3 (Slack notification) fails, Inngest retries only step 3 without re-running expensive LLM calls.
- Cons: Slightly steeper learning curve and proprietary SDK.
3. BullMQ + Redis (Best for Self-Hosted High Volume)
- How it works: Traditional Redis-backed message queue running in persistent Node.js worker processes (e.g. on Railway, Render, or ECS).
- Pros: Full control over concurrency, zero external vendor dependencies.
- Cons: Requires managing a persistent Linux container 24/7—ruining the simplicity of serverless hosting.
Practical Implementation: QStash + Supabase in 3 Steps
Here is the straightforward pattern we frequently deploy for Araho client MVPs:
Step 1: Create Job Record & Dispatch to Queue
// app/api/reports/generate/route.ts
import { NextResponse } from 'next/server';
import { Client } from '@upstash/qstash';
import { supabaseAdmin } from '@/lib/supabase/admin';
const qstash = new Client({ token: process.env.QSTASH_TOKEN! });
export async function POST(req: Request) {
const { ticker, userId } = await req.json();
// 1. Create initial job state in Postgres
const { data: job, error } = await supabaseAdmin
.from('ai_jobs')
.insert({
user_id: userId,
status: 'pending',
metadata: { ticker },
})
.select()
.single();
if (error) return NextResponse.json({ error: error.message }, { status: 500 });
// 2. Publish message to QStash pointing to your worker webhook
await qstash.publishJSON({
url: `${process.env.NEXT_PUBLIC_APP_URL}/api/workers/process-ai-job`,
body: { jobId: job.id, ticker },
retries: 3,
});
// 3. Return immediately to the client
return NextResponse.json({ jobId: job.id, status: 'queued' });
}
Step 2: Process the Heavy AI Workload in Worker
// app/api/workers/process-ai-job/route.ts
import { Receiver } from '@upstash/qstash';
import { NextResponse } from 'next/server';
import { supabaseAdmin } from '@/lib/supabase/admin';
import { generateDeepFinancialAnalysis } from '@/lib/ai/pipeline';
const receiver = new Receiver({
currentSigningKey: process.env.QSTASH_CURRENT_SIGNING_KEY!,
nextSigningKey: process.env.QSTASH_NEXT_SIGNING_KEY!,
});
export async function POST(req: Request) {
// 1. Verify cryptographic signature from QStash
const signature = req.headers.get('upstash-signature')!;
const body = await req.text();
const isValid = await receiver.verify({ signature, body });
if (!isValid) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const { jobId, ticker } = JSON.parse(body);
try {
// 2. Mark job as running
await supabaseAdmin
.from('ai_jobs')
.update({ status: 'processing', started_at: new Date() })
.eq('id', jobId);
// 3. Execute long-running multi-step LLM operations (e.g. 45s)
const report = await generateDeepFinancialAnalysis(ticker);
// 4. Save result and mark completed
await supabaseAdmin
.from('ai_jobs')
.update({
status: 'completed',
result: report,
completed_at: new Date(),
})
.eq('id', jobId);
return NextResponse.json({ success: true });
} catch (err: any) {
await supabaseAdmin
.from('ai_jobs')
.update({ status: 'failed', error_message: err.message })
.eq('id', jobId);
return NextResponse.json({ error: err.message }, { status: 500 });
}
}
Step 3: Deliver Instant Updates to the Frontend
Instead of writing complex WebSockets from scratch, you have two elegant choices:
- Lightweight Polling (Simple): Use TanStack Query (React Query) with
refetchInterval: (data) => data.status === 'completed' ? false : 2000. It stops polling automatically once the job finishes. - Supabase Realtime (Instant): Subscribe to
postgres_changeson theai_jobstable forid=eq.${jobId}. The moment the worker updates the row, the client re-renders without any manual polling.
3 Critical Rules for AI Background Queues
- Always Set Idempotency Keys: Network hiccups happen. If QStash retries a message, ensure your worker checks whether the job is already marked
completedbefore re-running a $0.50 LLM query. - Chunk Large Payloads into Storage: Never pass 50MB PDF binaries through the queue payload. Upload the file to S3/Supabase Storage first, and pass the file URL/storage key through the queue.
- Show Progressive UI Feedback: Don't leave users staring at an indeterminate spinner for 60 seconds. Update the
metadatacolumn in your database with progress stages: "Analyzing balance sheet...", "Evaluating 5-year FCF...", "Drafting executive summary...".
Need Production-Grade AI Architecture Built Right?
Building AI products that handle traffic spikes, long-running completions, and database concurrency requires practical senior engineering.
At Araho Digital, we specialize in shipping production-ready AI SaaS applications in two weeks for a flat $4,500. No junior interns, no over-engineered microservices, and no surprise hourly bills.
Explore our live products on our Apps Showcase, or use our MVP Scope & Cost Calculator to get an instant breakdown of your build.
Araho Digital
We build what we write about.
Every technique in this post was used on a real client project. If you're building a SaaS product or internal tool and want it done in weeks, not months — that's what we do.
Fixed price. Fixed scope. Money-back guarantee.