How MobileAudit Uses Celery Task Queue for Asynchronous APK Analysis with Progress Tracking

MobileAudit offloads APK inspections to a Celery task queue, using incremental update_state() calls to broadcast progress percentages and status messages that a lightweight HTTP endpoint polls to render real-time progress bars.

MobileAudit is an open-source Django application designed for automated Android security auditing. When users submit APK files for analysis, the application performs resource-intensive operations including decompilation, manifest parsing, and vulnerability scanning without blocking the web server by implementing an asynchronous task queue using Celery with real-time progress tracking.

Architecture Overview

The asynchronous workflow follows a producer-consumer pattern with state persistence:

  1. The Django view acts as a producer, enqueueing tasks via task_create_scan.delay()
  2. Celery workers consume tasks and execute analysis.analyze_apk()
  3. The analysis routine updates task metadata using self.update_state()
  4. A polling endpoint retrieves the latest state from the Celery result backend
  5. The frontend renders progress using the current and total values

Task Initiation and Queueing

When a user submits a scan, the create_scan view in app/views.py persists the scan record and immediately delegates processing to Celery.


# app/views.py (lines 78-84)

if form.is_valid():
    scan = form.save(commit=False)
    scan.user = request.user
    scan.status = 'In Progress'
    scan.progress = 1
    scan.save()
    task_id = task_create_scan.delay(scan.id)   # enqueue to Celery

    scan.task = task_id.id                      # store AsyncResult id

    scan.save()

The view stores the returned AsyncResult.id in the Scan model's task field, enabling later retrieval for progress polling.

Celery Worker Implementation

The task_create_scan function in app/worker/tasks.py initializes the task state before invoking the analysis routine.


# app/worker/tasks.py (lines 10-14)

@shared_task(bind=True)
def task_create_scan(self, scan_id):
    # initialise progress state

    self.update_state(
        state='STARTED',
        meta={'current': 1, 'total': 100, 'status': 'In Progress'}
    )
    # delegate to analysis module

    analysis.analyze_apk(self, scan_id)

Using bind=True allows access to self (the task instance), which provides the update_state() method for broadcasting progress metadata.

Progress Tracking During Analysis

The analyze_apk function in app/analysis.py (lines 44-108) performs the heavy lifting and updates the Celery task state at each major stage.


# app/analysis.py (excerpt showing progress updates)

def analyze_apk(task, scan_id):
    scan = Scan.objects.get(pk=scan_id)
    
    # Initial stage: 5%

    scan.progress = 5
    scan.status = 'Getting info of apk'
    scan.save()
    task.update_state(
        state='STARTED',
        meta={'current': scan.progress, 'total': 100, 'status': scan.status}
    )
    
    # After manifest parsing: ~20%

    # ... manifest analysis logic ...

    scan.progress = 20
    scan.status = 'Decompiling'
    scan.save()
    task.update_state(
        state='STARTED',
        meta={'current': scan.progress, 'total': 100, 'status': scan.status}
    )
    
    # After decompilation: ~40%

    # ... decompilation logic ...

    scan.progress = 40
    scan.status = 'Finding vulnerabilities'
    scan.save()
    task.update_state(
        state='STARTED',
        meta={'current': scan.progress, 'total': 100, 'status': scan.status}
    )
    
    # Final completion: 100%

    scan.progress = 100
    scan.status = 'Finished'
    scan.finished_on = datetime.now()
    scan.save()
    task.update_state(
        state='STARTED',
        meta={'current': scan.progress, 'total': 100, 'status': scan.status}
    )

Each update_state() call writes a JSON payload to the Celery result backend (typically Redis or RabbitMQ), persisting the current progress percentage, total target (100), and descriptive status text.

Real-Time Progress Polling

The scan_state view in app/worker/tasks.py (lines 16-24) provides an HTTP endpoint for the frontend to retrieve current progress.


# app/worker/tasks.py (lines 16-24)

def scan_state(request, id):
    scan = Scan.objects.get(pk=id)
    job = AsyncResult(scan.task)          # reconstruct AsyncResult from stored id

    data = job.info if job.info else job.result
    return HttpResponse(
        json.dumps(data), 
        content_type='application/json'
    )

The URL configuration in app/config/urls.py (line 58) exposes this at /scan_state/<int:id>:


# app/config/urls.py (line 58)

path('scan_state/<int:id>', tasks.scan_state, name='scan_state'),

Frontend Integration

The frontend JavaScript in app/templates/scan.html polls the /scan_state/<scan_id> endpoint at regular intervals. Each response contains the current and total values required to update the progress bar width and status text, providing users with real-time visibility into the decompilation and vulnerability scanning stages.

Summary

  • MobileAudit delegates resource-intensive APK analysis to Celery workers via task_create_scan.delay() to prevent web request blocking
  • The Celery task initializes progress state and invokes analysis.analyze_apk(), which updates progress via self.update_state() at each analysis stage
  • Progress metadata including current percentage and status descriptions are persisted to the Celery result backend (Redis/RabbitMQ)
  • The scan_state endpoint retrieves live progress by reconstructing an AsyncResult from the stored task ID and returning the metadata as JSON
  • The frontend renders real-time progress bars by polling the state endpoint and updating the UI with the returned percentage values

Frequently Asked Questions

How does MobileAudit prevent the web server from blocking during APK analysis?

MobileAudit prevents blocking by immediately enqueueing analysis jobs to a Celery worker pool using task_create_scan.delay(). This returns an AsyncResult ID to the Django view, allowing the HTTP response to complete while the heavy processing continues asynchronously in a separate worker process.

What Celery method does MobileAudit use to track analysis progress?

The application uses self.update_state() within the bound task instance. During analyze_apk(), the code calls update_state() with a meta dictionary containing current (percentage complete), total (100), and status (descriptive text), which Celery persists to the configured result backend.

How does the frontend retrieve real-time progress updates?

The frontend polls the /scan_state/<scan_id> HTTP endpoint, implemented in app/worker/tasks.py. This view reconstructs the Celery task using AsyncResult(scan.task), retrieves the latest metadata via job.info, and returns it as JSON. The frontend uses the current and total values to update the progress bar and status text.

Where is the Celery task ID stored for progress polling?

The task ID is stored in the task field of the Scan Django model. Immediately after calling task_create_scan.delay() in app/views.py, the application saves the returned AsyncResult.id to scan.task, enabling the scan_state endpoint to later reconstruct the AsyncResult object and retrieve the current progress.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →