Find total attachment size per project in Jira Cloud using REST API

Platform Notice: Cloud Only - This article only applies to Atlassian apps on the cloud platform.

Summary

Currently, in Jira Cloud, Storage can be tracked per product but not per space.

Reference: https://support.atlassian.com/security-and-access-policies/docs/track-storage-and-move-data-across-products/

However, you can approximate this by using the Jira Cloud REST API to search for work items with attachments and then programmatically sum the attachment sizes grouped by space.

Solution

Prerequisites

  1. Jira Cloud site URL (e.g. https://your-domain.atlassian.net)

  2. Jira Cloud user account with:

    • Permission to browse the spaces/work items you want to measure

    • Permission to see attachments

  3. Jira API token for that account (Create from: https://id.atlassian.com/manage-profile/security/api-tokens)

  4. Python 3 installed

  5. Requests library installed:

    pip install requests

Python script

  1. Save the following script as jira_attachment_size_per_project.py (or any name you prefer):

    import requests from requests.auth import HTTPBasicAuth import math # ========= Configuration ========= JIRA_BASE_URL = "https://your-domain.atlassian.net" # No trailing slash EMAIL = "your-email@example.com" API_TOKEN = "your-api-token" # Specify scope here with JQL # e.g. issues with attachments in all projects: 'attachments IS NOT EMPTY' # e.g. specific projects only: 'project in (PROJ1, PROJ2) AND attachments IS NOT EMPTY' JQL = 'attachments IS NOT EMPTY' # Max results per request MAX_RESULTS = 100 # ======================= def _jira_get(url, params=None): """GET with auth and error reporting.""" headers = {"Accept": "application/json"} auth = HTTPBasicAuth(EMAIL, API_TOKEN) response = requests.get(url, params=params, headers=headers, auth=auth) if not response.ok: print(f"HTTP {response.status_code} for {response.url}") if response.text: print(f"Response body: {response.text[:1000]}") response.raise_for_status() return response.json() def _jira_post(url, json_body): """POST JSON with auth and error reporting.""" headers = {"Accept": "application/json", "Content-Type": "application/json"} auth = HTTPBasicAuth(EMAIL, API_TOKEN) response = requests.post(url, json=json_body, headers=headers, auth=auth) if not response.ok: print(f"HTTP {response.status_code} for {response.url}") if response.text: print(f"Response body: {response.text[:1000]}") response.raise_for_status() return response.json() def search_issues_jql(max_results=100, next_page_token=None, fields=None): """ Fetch issues by JQL via POST /rest/api/3/search/jql (enhanced search). Uses nextPageToken for pagination; no startAt (see API docs). """ if fields is None: fields = ["project", "attachment"] elif isinstance(fields, str): fields = [f.strip() for f in fields.split(",")] url = f"{JIRA_BASE_URL}/rest/api/3/search/jql" body = { "jql": JQL, "maxResults": max_results, "fields": fields, } if next_page_token is not None: body["nextPageToken"] = next_page_token return _jira_post(url, body) def get_issue(issue_key, fields=None): """Fetch a single issue (for attachment data).""" if fields is None: fields = ["project", "attachment"] url = f"{JIRA_BASE_URL}/rest/api/3/issue/{issue_key}" params = {"fields": ",".join(fields) if isinstance(fields, list) else fields} return _jira_get(url, params) def _sum_attachment_sizes_from_issue(issue, project_sizes): """Add attachment sizes from one issue into project_sizes.""" fields = issue.get("fields", {}) project = fields.get("project") or {} project_key = project.get("key") or "UNKNOWN" attachments = fields.get("attachment", []) or [] for att in attachments: size = att.get("size", 0) if isinstance(size, int): project_sizes[project_key] = project_sizes.get(project_key, 0) + size def calculate_attachment_size_grouped_by_project(): """ Iterate over all issues matching the JQL and sum attachment sizes per project. Uses enhanced search pagination (nextPageToken / isLast). Returns: dict[project_key] = total_size_in_bytes """ project_sizes = {} next_page_token = None while True: data = search_issues_jql( max_results=MAX_RESULTS, next_page_token=next_page_token, fields=["project", "attachment"], ) issues = data.get("issues", []) for issue in issues: _sum_attachment_sizes_from_issue(issue, project_sizes) if data.get("isLast", True): break next_page_token = data.get("nextPageToken") if not next_page_token: break return project_sizes def format_bytes(num_bytes): """ Convert byte count to B, KB, MB, GB, TB notation. """ if num_bytes == 0: return "0 B" units = ["B", "KB", "MB", "GB", "TB"] idx = int(math.floor(math.log(num_bytes, 1024))) value = num_bytes / math.pow(1024, idx) return f"{value:.2f} {units[idx]}" def main(): print(f"Running JQL: {JQL}") project_sizes = calculate_attachment_size_grouped_by_project() print("\n=== Total attachment size per project (based on JQL) ===") for project_key, size_bytes in sorted(project_sizes.items()): print(f"{project_key}: {size_bytes} bytes ({format_bytes(size_bytes)})") if __name__ == "__main__": main()

Note: The script pages through all matching work items automatically using the enhanced search's nextPageToken and stops when isLast is true, so it captures every issue in scope, not just the first MAX_RESULTS (100).

Configure the script

  1. In the script, update the following values:

    • JIRA_BASE_URL Replace with your Jira Cloud base URL, for example:

      JIRA_BASE_URL = "https://example.atlassian.net"

    • EMAIL The email address of the Jira Cloud user that owns the API token:

      EMAIL = "your-email@example.com"

    • API_TOKEN The API token generated for that user:

      API_TOKEN = "your-api-token"

    • JQL Adjust the query to define the scope you want to measure:

      • All work items with attachments (all spaces):

        JQL = 'attachments IS NOT EMPTY'

      • Only specific spaces:

        JQL = 'project in (PROJ1, PROJ2) AND attachments IS NOT EMPTY'

      • Add time or other filters if needed, for example:

        JQL = 'project = PROJ1 AND attachments IS NOT EMPTY AND updated >= -90d'

  2. Run the script

    From a terminal in the directory where you saved the script:

    python jira_attachment_size_per_project.py

    The output will look similar to:

    Running JQL: attachments IS NOT EMPTY === Total attachment size per project (based on JQL) === PROJ1: 123456789 bytes (117.74 MB) PROJ2: 987654321 bytes (942.10 MB)

    To sanity-check a space's total, open Jira and run the same JQL from the article for that single space (for example project = PROJ1 AND attachments IS NOT EMPTY) in the work item search. The work items returned are the same set the script sums for that space, so the script's PROJ1 byte total corresponds to the attachments across exactly those issues.

Updated on August 27, 2026

Still need help?

The Atlassian Community is here for you.