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

> Discover how MobileAudit leverages Celery for asynchronous APK analysis, featuring real-time progress tracking through incremental state updates and HTTP polling.

- Repository: [Mónica Pastor/mobileaudit](https://github.com/mpast/mobileaudit)
- Tags: internals
- Published: 2026-03-07

---

**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`](https://github.com/mpast/mobileaudit/blob/main/app/views.py) persists the scan record and immediately delegates processing to Celery.

```python

# 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`](https://github.com/mpast/mobileaudit/blob/main/app/worker/tasks.py) initializes the task state before invoking the analysis routine.

```python

# 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`](https://github.com/mpast/mobileaudit/blob/main/app/analysis.py) (lines 44-108) performs the heavy lifting and updates the Celery task state at each major stage.

```python

# 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`](https://github.com/mpast/mobileaudit/blob/main/app/worker/tasks.py) (lines 16-24) provides an HTTP endpoint for the frontend to retrieve current progress.

```python

# 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`](https://github.com/mpast/mobileaudit/blob/main/app/config/urls.py) (line 58) exposes this at `/scan_state/<int:id>`:

```python

# 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`](https://github.com/mpast/mobileaudit/blob/main/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`](https://github.com/mpast/mobileaudit/blob/main/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`](https://github.com/mpast/mobileaudit/blob/main/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.