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:
| work | Why it takes so long |
|---|---|
| PDF parsing | File size, OCR, table/image processing |
| Chunking | Document structure analysis required |
| Create Embedding | External model call or batch processing |
| Save Vector | Bulk insert, index update |
| Bulk summary | Increase number of model calls |
| Run Eval | Repeated 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.
| column | explanation |
|---|---|
job_id | task id |
job_type | DOCUMENT_INDEX, EVAL_RUN, etc. |
idempotency_key | Duplicate prevention key |
status | PENDING, RUNNING, SUCCEEDED, FAILED |
attempt_count | Number of retries |
last_error_code | Last cause of failure |
created_at | creation time |
updated_at | update 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.
| error | Retry? |
|---|---|
| External API timeout | possible |
| rate limit | Yes, backoff required |
| Invalid document format | impossibility |
| schema validation failed | Conditionally available |
| No permission | impossibility |
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
| characteristic | meaning |
|---|---|
queue_depth | Number of waiting messages |
job_duration_p95 | Delay in processing tasks |
job_failure_rate | failure rate |
retry_count | Number of retries |
dlq_message_count | Failure requiring analysis |
duplicate_request_count | Duplication 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
- AWS Builders Library — Making retries safe with idempotent APIs:https://aws.amazon.com/builders-library/making-retries-safe-with-idempotent-APIs/
- Amazon SQS Visibility Timeout:https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-visibility-timeout.html
- Amazon SQS Dead-letter Queues:https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-dead-letter-queues.html
- Apache Kafka Documentation:https://kafka.apache.org/documentation/
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.
| situation | treatment |
|---|---|
| Re-request the same version of the same document | Return existing job |
| New version of the same document | Create new job |
| Retry after worker timeout | Prevent duplication with idempotency key |
| Documents that keep failing | Go 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를 통해 운영됩니다.