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.
However, you can approximate this by using the Jira Cloud REST API to search for issues with attachments and then programmatically sum the attachment sizes grouped by project.
Solution
Prerequisites
Jira Cloud site URL (e.g.
https://your-domain.atlassian.net)Jira Cloud user account with:
Permission to browse the projects/issues you want to measure
Permission to see attachments
Jira API token for that account (Create from: https://id.atlassian.com/manage-profile/security/api-tokens)
Python 3 installed
requestslibrary installed:pip install requests
Python script
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()
Configure the script
In the script, update the following values:
JIRA_BASE_URLReplace with your Jira Cloud base URL, for example:JIRA_BASE_URL = "https://example.atlassian.net"EMAILThe email address of the Jira Cloud user that owns the API token:EMAIL = "your-email@example.com"API_TOKENThe API token generated for that user:API_TOKEN = "your-api-token"JQLAdjust the query to define the scope you want to measure:All issues with attachments (all projects):
JQL = 'attachments IS NOT EMPTY'Only specific projects:
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'
Run the script
From a terminal in the directory where you saved the script:
python jira_attachment_size_per_project.pyThe 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)
Was this helpful?