Skip to content

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.

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 pending status 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-retry runs 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.
  1. Webapp uploads file → creates pdf_documents row (status pending) → calls pdf-api to start processing.
  2. 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.status to completed (and writes extracted_orders).
  3. Webapp shows the “Extracting order details…” spinner while pdf_documents.status is pending or processing.
  4. Frontend polls the webapp API GET /api/pdf-documents/{documentId} every 3 seconds. When the response has status === '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.status to completed (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.

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:
    • Metadatadocument_id (UUID of the pdf_documents row).
    • Metadatauser_id (Clerk user ID).
    • Metadatachunk_number / total_chunks (for multi-chunk docs, ensure the last chunk completed).
  • 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_chunk traces 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.

Using the document_id from Langfuse (or from the customer’s URL):

SELECT id, status, processing_error, updated_at, clerk_organization_id, user_id
FROM pdf_documents
WHERE 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 no processing_error are written. Check Celery/Redis and worker logs for this document_id or task_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 to completed. 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.organization is set (otherwise 400 “Organization not found”).
    • The document’s clerk_organization_id equals locals.organization.id.
    • The document’s user_id is 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_id set 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, organizationId so you can see “this user got 404 for this document.”
  • In DB: same document_id → confirm clerk_organization_id and user_id. Compare with the customer’s Clerk org and user id (from Langfuse user_id or support).

If Langfuse shows successful extraction but DB still has pending/processing:

  • Worker logs: search by document_id for “Failed to update document status to COMPLETED” or “Database status update failed” / “timed out.”
  • Possible causes: DB timeout, connection pool, or large original_data causing the update to fail (worker then tries a fallback without original_data; if that too fails, status never flips to completed).

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”
  1. Open the trace for the customer’s processing (e.g. the process_single_chunk or parent trace).
  2. In Metadata (or Parsed Output / custom fields), note:
    • document_id → use in DB and in log search.
    • user_id → Clerk user; compare with document’s user_id and 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.

  • Refresh the page: If the DB is already completed, a full reload will load the document with status: completed and show orders (no polling needed).
  • Re-upload: If the document row is stuck in pending/processing and 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.
  • Frontend polling: apps/webapp/src/components/orders/DocumentOrderReview.tsx (polling GET /api/pdf-documents/${pdfDocument.id}, isProcessing from $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 without original_data; errors logged as “Failed to update document status to COMPLETED (non-fatal)”).

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.