The Long-Running & Asynchronous Operations pattern is used when an operation cannot reasonably complete within a single synchronous HTTP request. Instead of blocking until work is done, the API:
- Accepts the request
- Creates a job or task
- Returns control to the client (usually with
202 Accepted) - Exposes an endpoint (and optionally webhooks) for checking job status and results
Common examples:
- Generating large reports
- Exporting data to files
- Running complex analytics jobs
- Kicking off heavy back-office workflows
9.1. Overview
Instead of:
POST /generate-report # wait 90 seconds 😬
You do this:
1. Submit job:
POST /generate-report
→ 202 Accepted + Location: /report-jobs/{jobId}
2. Poll for status:
GET /report-jobs/{jobId}
→ status: PENDING | RUNNING | COMPLETED | FAILED
3. Optionally download result (e.g., file URL) once the job is COMPLETED.
Key characteristics:
- Decouples request/response from work execution
- Avoids timeouts, gateway limits, and poor user experience
- Allows progress tracking and retrying jobs without resending full input
9.2. When to Use the Long-Running & Async Pattern
Use this pattern when:
- The operation may take several seconds or minutes.
- You’re doing heavy computation, I/O, or multi-step workflows.
- You’re generating large reports or exports.
- You’re orchestrating multiple external systems with uncertain latencies.
- You anticipate hitting timeout limits (API gateways, load balancers, clients).
Examples:
POST /generate-reportfor a large orders report.POST /export-customersto CSV in object storage.POST /run-analytics-jobfor batch analytics.
9.3. When NOT to Use This Pattern
Avoid async jobs when:
- The operation is fast enough (well under your timeout thresholds).
- Clients strongly prefer immediate feedback (small, synchronous operations).
- You’re trying to hide bad performance instead of fixing it.
- The task is actually a resource lifecycle change (Extended CRUD may fit better).
Also be careful not to:
- Use async jobs for tiny write operations just because it “feels enterprisey.”
- Overcomplicate the API for operations that are naturally synchronous.
9.4. What the Pattern Looks Like
A canonical shape:
- Job submission endpoint (usually
POST)
POST /generate-report
Content-Type: application/json
{
"reportType": "ORDERS_SUMMARY",
"dateRange": {
"fromDate": "2024-01-01",
"toDate": "2024-01-31"
},
"callbackUrl": "https://client.example.com/hooks/report-completed"
}
Response:
202 Accepted
Location: /report-jobs/job_12345
{
"jobId": "job_12345",
"status": "PENDING"
}
- Job status endpoint
GET /report-jobs/job_12345
{
"jobId": "job_12345",
"status": "COMPLETED",
"downloadUrl": "https://files.example.com/reports/job_12345.csv"
}
- Optionally, the server POSTs to
callbackUrlwhen the job completes.
9.5. Anti-Patterns to Avoid
❌ Blocking until work completes anyway
- Returning
200 OKafter a 90-second report generation defeats the purpose. - Gateways or clients may give up long before the server finishes.
❌ Not exposing a job resource
- If the client has no way to check job status, it’s flying blind.
- Always provide a job ID and a status endpoint.
❌ Overloading “fire-and-forget” with no feedback
- “We accepted your request, good luck.”
- Provide at least eventual success/failure via job status or webhooks.
❌ Using jobs for simple CRUD
- Don’t wrap simple writes in jobs — that’s just indirection.
9.6. OpenAPI Example
A lean but complete OpenAPI 3.0.3 document:
POST /generate-report— submits a report jobGET /report-jobs/{jobId}— retrieves job status and result metadata
openapi: 3.0.3
info:
title: Reports API - Long-Running & Async Pattern Example
version: 1.0.0
servers:
- url: https://api.example.com
paths:
/generate-report:
post:
summary: Submit a report generation job
tags: [Reports]
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/GenerateReportRequest'
examples:
default:
value:
reportType: "ORDERS_SUMMARY"
dateRange:
fromDate: "2024-01-01"
toDate: "2024-01-31"
format: "CSV"
callbackUrl: "https://client.example.com/hooks/report-completed"
responses:
'202':
description: Report job accepted for asynchronous processing
headers:
Location:
schema:
type: string
example: https://api.example.com/report-jobs/job_12345
content:
application/json:
schema:
$ref: '#/components/schemas/ReportJob'
examples:
default:
value:
jobId: "job_12345"
status: "PENDING"
/report-jobs/{jobId}:
get:
summary: Get status of a report job
tags: [Reports]
parameters:
- in: path
name: jobId
required: true
schema:
type: string
example: "job_12345"
responses:
'200':
description: Report job status
content:
application/json:
schema:
$ref: '#/components/schemas/ReportJob'
examples:
pending:
value:
jobId: "job_12345"
status: "RUNNING"
downloadUrl: null
completed:
value:
jobId: "job_12345"
status: "COMPLETED"
downloadUrl: "https://files.example.com/reports/job_12345.csv"
failed:
value:
jobId: "job_12345"
status: "FAILED"
downloadUrl: null
errorCode: "REPORT_GENERATION_FAILED"
errorMessage: "Downstream data source unavailable."
'404':
description: Job not found
components:
schemas:
GenerateReportRequest:
type: object
properties:
reportType:
type: string
example: "ORDERS_SUMMARY"
dateRange:
type: object
properties:
fromDate:
type: string
format: date
example: "2024-01-01"
toDate:
type: string
format: date
example: "2024-01-31"
required:
- fromDate
- toDate
format:
type: string
enum: [CSV, PDF, JSON]
example: "CSV"
callbackUrl:
type: string
nullable: true
example: "https://client.example.com/hooks/report-completed"
required:
- reportType
- dateRange
- format
ReportJob:
type: object
properties:
jobId:
type: string
example: "job_12345"
status:
type: string
enum: [PENDING, RUNNING, COMPLETED, FAILED]
example: "PENDING"
downloadUrl:
type: string
nullable: true
example: "https://files.example.com/reports/job_12345.csv"
errorCode:
type: string
nullable: true
example: "REPORT_GENERATION_FAILED"
errorMessage:
type: string
nullable: true
example: "Downstream data source unavailable."
required:
- jobId
- status
9.7. Visualizing the Long-Running & Async Pattern (Mermaid)**
sequenceDiagram
autonumber
participant C as Client
participant API as Reports API
participant Q as Job Queue
participant W as Report Worker
participant DS as Data Store
Note over C,API: Submit long-running job
C->>API: POST /generate-report<br/>{ reportType, dateRange, format, callbackUrl }
API->>Q: enqueue(report job)
API-->>C: 202 Accepted<br/>Location: /report-jobs/job_12345<br/>{ jobId, status: PENDING }
Note over Q,W: Background processing
Q->>W: deliver job_12345
W->>DS: fetch data for report
DS-->>W: data rows
W-->>API: update job_12345 status = COMPLETED + downloadUrl
Note over C,API: Poll for status (or receive callback)
C->>API: GET /report-jobs/job_12345
API-->>C: 200 OK<br/>{ status: COMPLETED, downloadUrl: ... }