Queues and Idempotency for Long-Running AI Work
메뉴

AI Backend

Queues and Idempotency for Long-Running AI Work

Reliable background execution for AI jobs that are too slow for request-response handling.

Queues and Idempotency for Long-Running AI Work hero image

Safely handle long-running AI tasks

In this article, we outline how to safely handle long-running AI tasks with queues and idempotence.

There are many tasks in LLM services that do not end immediately upon request. Parsing a PDF, splitting the document into chunks, creating embeddings, storing them in a vector DB, and summarizing large documents can take anywhere from seconds to minutes.

If these tasks are processed synchronously within an API request, users will wait a long time, the server will easily become saturated, and recovery in case of failure will be difficult. So we need a queue.

But the moment you use a cue, another problem arises. Messages can be duplicated, retried, and workers can die while processing. So idempotency is needed.

Analysis Base Date: 2026-05-12 Lab Base Environment: FastAPI, PostgreSQL, Redis Queue, or SQS/Kafka Concepts Key References: AWS Builders Library, Amazon SQS Docs, Kafka Docs

Key takeaways

  • Long-running AI tasks should be separated from API requests.
  • Queues absorb failures and load, but create the possibility of redundant processing.
  • The idempotency key prevents the results from being corrupted even if the same task is executed multiple times.
  • Task state tables increase observability of asynchronous processing.
  • Retry and DLQ are not devices that hide failures, but leave them in a state that can be analyzed.

1. Why AI tasks take so long

Long-running operations in the AI ​​backend include:

workWhy it takes so long
PDF parsingFile size, OCR, table/image processing
ChunkingDocument structure analysis required
Create EmbeddingExternal model call or batch processing
Save VectorBulk insert, index update
Bulk summaryIncrease number of model calls
Run EvalRepeated calls to test dataset

These tasks are difficult to tie to the same lifecycle as user requests.

2. Problems with synchronous processing

Synchronous processing is simple at first.

# This is an example.POST /documents/{id}/indexto PDF parsing→ Chunking→ EmbeddingSave to Vectorto reply

But a problem arises.

# This is an example.[ ] The request time becomes longer.[ ] You may experience a reverse proxy timeout.[ ] The user cannot know the progress status.[ ] In case of failure, it is difficult to know how far it has been dealt with.[ ] If the same request comes in again, duplicate indexes may occur.

So we need to change it to asynchronous processing.

3. Queue-based processing flow

# This is an example.Client→ POST /documents/{id}/index→ API server creates job→ enqueue message→ return job_id→ Worker consumes message→ process indexing→ update job status→ Client polls GET /jobs/{job_id}

The advantage of this structure is that the API server does not grab long operations directly. The user receivesjob_idand queries the status.

4. idempotency key Design

Indexing the same document with the same version must be done only once.

# This is an example.idempotency_key = document_id + document_version + pipeline_version

example:

# This is an example.doc_123:v7:rag-pipeline-v2

If a request comes in again with the same key, the existing job is returned without creating a new job.

# This is example code.async def create_index_job(document_id: str, document_version: int):    key = f"{document_id}:v{document_version}:rag-pipeline-v2"     existing = await job_repo.find_by_idempotency_key(key)    if existing:        return existing     job = await job_repo.create(        type="DOCUMENT_INDEX",        idempotency_key=key,        status="PENDING",    )    await queue.publish({"job_id": job.id})    return job

5. Task status table design

Asynchronous operations must store state.

columnexplanation
job_idtask id
job_typeDOCUMENT_INDEX, EVAL_RUN, etc.
idempotency_keyDuplicate prevention key
statusPENDING, RUNNING, SUCCEEDED, FAILED
attempt_countNumber of retries
last_error_codeLast cause of failure
created_atcreation time
updated_atupdate time

Without a state table, it is impossible to know “how far things have gone” when a worker fails.

6. Retry and Backoff

Retries are necessary, but infinite retries are dangerous. Especially in an external model API failure situation, if all workers retry immediately, the failure may increase.

Recommended Policy:

# This is an example.1st failure: Retry in 5 secondsSecond failure: Retry in 30 seconds3rd failure: Retry in 2 minutes4th failure: DLQ move

You should also distinguish between retriable and non-retriable errors.

errorRetry?
External API timeoutpossible
rate limitYes, backoff required
Invalid document formatimpossibility
schema validation failedConditionally available
No permissionimpossibility

7. DLQ design

DLQ is not a place to dump failure messages. This is where it is isolated so that it can be analyzed.

Messages entering the DLQ must contain the following information:

// This is an example JSON structure.{  "job_id": "job_123",  "idempotency_key": "doc_123:v7:rag-pipeline-v2",  "attempt_count": 4,  "last_error_code": "EMBEDDING_RATE_LIMIT",  "last_error_message": "Rate limit exceeded",  "trace_id": "0af..."}

DLQ messages should be associated with notifications, dashboards, and reprocessing tools.

8. Document index example

# This is an example.1. User uploads document2. API creates document_version3. Create index job4. queue publish5. Worker parses document6. Chunk creation7. Create embedding8. Save to pgvector9. Change job status to SUCCEEDED

On failure:

# This is an example.worker timeoutto message visibility timeout expiresto be reprocessed by another workerto check the same idempotency keyto skip chunks that have already been processedDLQ movement when to repeat failure

9. Operational indicators

characteristicmeaning
queue_depthNumber of waiting messages
job_duration_p95Delay in processing tasks
job_failure_ratefailure rate
retry_countNumber of retries
dlq_message_countFailure requiring analysis
duplicate_request_countDuplication prevented by idempotency

10. Practical checklist

# This is an example.[ ] Have you separated long-running tasks from API requests?[ ] Can I check the status by job_id?[ ] Is idempotency_key designed for the unit of work?[ ] Can processing continue even after restarting the worker?[ ] retry Did you distinguish between possible and impossible errors?[ ] Is there a backoff and maximum number of retries?[ ] Does the DLQ message include trace_id and error_code?[ ] Is there a DLQ reprocessing procedure?

11. Q&A

Q1. Should I use Redis Queue, SQS, or Kafka?

For initial learning projects, Redis Queue is sufficient. In production environments, consider SQS or Kafka depending on durability, retries, DLQ, and throughput requirements.

Q2. Is DB unique constraint sufficient for idempotency?

In many cases, unique constraints are a good starting point. However, a separate job table is needed to handle job status, retries, and partial successes together.

Q3. Can we assume that messages will only be processed once?

It's safer not to assume that. In a queue-based system, it is realistic to design it to be idempotent, assuming the possibility of redundancy.

12. References and uncertainty

References

uncertainty

  • Queue selection will depend on traffic, durability, and team operating experience.
  • Since exactly-once processing semantics are difficult to guarantee throughout the system, idempotency in business outcome criteria must be designed first.

Practical example: Avoid duplicate execution of document indexing operations

Document embedding operations can easily be duplicated when a user presses a button twice or a worker retries after a timeout. The idempotency key is usually created by combiningtenant_id,document_id,document_version, andembedding_model. If the document version and model are the same, a job already running is returned. If the document or embedding model changes, a new job must be created.

situationtreatment
Re-request the same version of the same documentReturn existing job
New version of the same documentCreate new job
Retry after worker timeoutPrevent duplication with idempotency key
Documents that keep failingGo to DLQ and check manually

A failure case is simply doing too many retries. There are failures that cannot be immediately resolved by retrying, such as provider rate limits or document parsing errors. The retry count, last error, and next retry time must be left in the job table, and sent to DLQ after a certain number of times. A queue is not a device to postpone slow work, but rather an operational layer that classifies failures and makes them reprocessable.

In the task screen, it is better to distinguish betweenqueued,running,retry_scheduled,failed, andcompletedrather than just showing "Processing" alone. When a user uploads the same document again, existing jobs must be displayed to reduce duplication costs.

댓글

GitHub 계정으로 로그인하면 댓글을 남길 수 있습니다. 댓글은 GitHub Discussions를 통해 운영됩니다.

TOP