Troubleshooting: Stuck Spinner After PDF Upload
When a customer reports that the order page keeps spinning after upload even though processing appears to complete (e.g. Langfuse shows successful process_single_chunk traces), use this guide to diagnose.
Root cause (most common)
Section titled “Root cause (most common)”Task lost due to Celery acks_late=False (now fixed)
With the default acks_late=False, Celery acknowledges tasks immediately when received by the worker, before processing starts. If the worker crashes/restarts/loses connection after acking but before processing:
- The task is already removed from the queue (was acked).
- No worker code runs, so no logs, no orders written, no error recorded.
- Document stays in
pendingstatus forever.
Fix applied:
task_acks_late=True– ack after processing, not before.task_reject_on_worker_lost=True– requeue if worker dies.visibility_timeout=900– task reappears in queue if not acked within 15 min.- Scheduled task
scheduled-stuck-document-retryruns every 5 min and auto-retries documents stuck in pending/processing for > 5 min. - UI shows “Restart extraction” after 45s of spinning so users can manually retry if needed.
Flow summary
Section titled “Flow summary”- Webapp uploads file → creates
pdf_documentsrow (statuspending) → calls pdf-api to start processing. - pdf-api enqueues a Celery task and returns 200 immediately. It does not wait for or log completion—that is expected. pdf-worker (separate service) picks up the task, processes the document, and updates
pdf_documents.statustocompleted(and writesextracted_orders). - Webapp shows the “Extracting order details…” spinner while
pdf_documents.statusispendingorprocessing. - Frontend polls the webapp API
GET /api/pdf-documents/{documentId}every 3 seconds. When the response hasstatus === 'completed', it stops polling and fetches orders; the spinner stops.
If you only see pdf-api logs: pdf-api will show “PDF processing task started” and then nothing else for that document. To see whether processing completed or failed, check pdf-worker logs (and the database) using the same document_id or task_id.
So the spinner only stops when the webapp API returns status: 'completed' for that document. If the customer is “stuck spinning,” one of the following is happening:
- The worker never set
pdf_documents.statustocompleted(e.g. status update failed after extraction). - The polling request never gets a successful 200 with
status: 'completed'(e.g. 404/401/400 so the frontend keeps polling and never sees completed). - Replication lag (if you use a read replica): worker writes to primary, webapp reads from replica that hasn’t updated yet.
Diagnostic checklist
Section titled “Diagnostic checklist”1. Confirm processing completed (Langfuse / worker)
Section titled “1. Confirm processing completed (Langfuse / worker)”- In Langfuse → Tracing, filter by:
- Environment: production (or the env the customer uses).
- Name:
process_single_chunk(and/or the top-level trace that represents the full document).
- Find the trace for the customer’s document using:
- Metadata →
document_id(UUID of thepdf_documentsrow). - Metadata →
user_id(Clerk user ID). - Metadata →
chunk_number/total_chunks(for multi-chunk docs, ensure the last chunk completed).
- Metadata →
- If you see a pdf-processor-api trace with Output: undefined, that’s the API accepting the job; it does not mean the worker finished. Rely on pdf-extraction-worker /
process_single_chunktraces and metadata (e.g.status: "completed", parsed output) to confirm extraction success.
So: document_id and user_id from Langfuse are the keys to correlate with DB and logs.
2. Check database state
Section titled “2. Check database state”Using the document_id from Langfuse (or from the customer’s URL):
SELECT id, status, processing_error, updated_at, clerk_organization_id, user_idFROM pdf_documentsWHERE id = '<document_id>';- status = ‘completed’ and updated_at recent → worker updated correctly. The problem is likely that the frontend is not getting this (e.g. 404/401 on
GET /api/pdf-documents/{id}). - status = ‘pending’ or ‘processing’ and no rows in
extracted_orders→ the worker never ran for this document (task not consumed from the queue, worker down, or wrong queue). No code path ran, so no orders and noprocessing_errorare written. Check Celery/Redis and worker logs for thisdocument_idortask_id. In the UI, after ~45 seconds of spinning, “Restart extraction” is shown so the user can re-trigger processing. - status = ‘pending’ or ‘processing’ but rows exist in
extracted_orders→ worker saved orders but failed to update the row tocompleted. Check worker logs for “Failed to update document status to COMPLETED (non-fatal)” or “Fallback status update also failed”. - status = ‘failed’ → processing failed; frontend should eventually show failure. If they only see spinner, see “Poll returns 404/401” below (e.g. they get 404 so the UI never transitions to failed).
3. Why the poll might not return 200 with completed
Section titled “3. Why the poll might not return 200 with completed”The webapp’s GET /api/pdf-documents/[id] returns 200 only if:
- The user is authenticated (
locals.user). - For non–support-admin users:
locals.organizationis set (otherwise 400 “Organization not found”).- The document’s
clerk_organization_idequalslocals.organization.id. - The document’s
user_idis null or equals the current user’s id.
If any of these fail, the API returns 400 or 404. The frontend treats non-ok as a transient error and keeps polling without showing an error, so the spinner never stops.
Typical “works for me, not for customer” causes:
- Organization mismatch: Document created under org A; customer is in org B (e.g. switched org, or different org in session). → 404.
- Missing organization in session: e.g. personal account or org not loaded in Clerk for that request. → 400.
- User/document mismatch: Document has
user_idset to another member; current user doesn’t match. → 404.
What to check:
- In logs, search for the diagnostic message added for 404/401 (see below): it includes
documentId,userId,organizationIdso you can see “this user got 404 for this document.” - In DB: same
document_id→ confirmclerk_organization_idanduser_id. Compare with the customer’s Clerk org and user id (from Langfuseuser_idor support).
4. Worker status update failed
Section titled “4. Worker status update failed”If Langfuse shows successful extraction but DB still has pending/processing:
- Worker logs: search by
document_idfor “Failed to update document status to COMPLETED” or “Database status update failed” / “timed out.” - Possible causes: DB timeout, connection pool, or large
original_datacausing the update to fail (worker then tries a fallback withoutoriginal_data; if that too fails, status never flips tocompleted).
5. Replication lag (if applicable)
Section titled “5. Replication lag (if applicable)”If the webapp reads from a read replica and the worker writes to the primary, a short period of “completed in DB but API still returns old status” is possible. Usually not “infinite” spinner; if it is, consider reading document status from primary for the polling endpoint or adding a short delay/retry.
Using Langfuse to get document_id and user_id
Section titled “Using Langfuse to get document_id and user_id”- Open the trace for the customer’s processing (e.g. the
process_single_chunkor parent trace). - In Metadata (or Parsed Output / custom fields), note:
document_id→ use in DB and in log search.user_id→ Clerk user; compare with document’suser_idand org membership.session_id/correlation_id→ use to tie to backend logs if needed.
Then run the SQL above and search your logs for that document_id (and optional user_id / organizationId) to see why the poll might be 404/401.
Quick fixes for the customer
Section titled “Quick fixes for the customer”- Refresh the page: If the DB is already
completed, a full reload will load the document withstatus: completedand show orders (no polling needed). - Re-upload: If the document row is stuck in
pending/processingand you can’t fix the worker update, they can upload again (creates a new document). - Org/context: If the issue is org or user mismatch, have them confirm they’re in the correct organization and, if applicable, that the document wasn’t uploaded by another user in a way that restricts visibility.
Code references
Section titled “Code references”- Frontend polling:
apps/webapp/src/components/orders/DocumentOrderReview.tsx(pollingGET /api/pdf-documents/${pdfDocument.id},isProcessingfrom$pdfDocument?.status). - Webapp API:
apps/webapp/src/pages/api/pdf-documents/[id].ts(auth + org + user filter; returns 404 when no matching document). - Worker status update:
apps/pdf-worker/tasks.py(update_pdf_document_status(..., status=PdfDocumentStatus.COMPLETED, ...)and fallback withoutoriginal_data; errors logged as “Failed to update document status to COMPLETED (non-fatal)”).
Logging added for diagnosis
Section titled “Logging added for diagnosis”To make “works for me, not for them” easier to debug:
- GET /api/pdf-documents/[id] logs a warning when it returns 404 or 401, including:
documentId,userId,organizationId(or missing org)- So you can correlate with Langfuse and DB and see that this user/org got 404 for this document.
Search logs by document_id (from Langfuse) and “pdf-documents 404” (or the message text) to quickly see if the customer’s polls are 404 due to org/user mismatch.