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:

  1. Accepts the request
  2. Creates a job or task
  3. Returns control to the client (usually with 202 Accepted)
  4. 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-report for a large orders report.
  • POST /export-customers to CSV in object storage.
  • POST /run-analytics-job for 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:

  1. 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"
}
  1. Job status endpoint
GET /report-jobs/job_12345

{
  "jobId": "job_12345",
  "status": "COMPLETED",
  "downloadUrl": "https://files.example.com/reports/job_12345.csv"
}
  1. Optionally, the server POSTs to callbackUrl when the job completes.

9.5. Anti-Patterns to Avoid

Blocking until work completes anyway

  • Returning 200 OK after 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 job
  • GET /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: ... }