Skip to main content

jupyterhub-storage-usage

JupyterHub service that monitors per-user storage usage, culls servers that exceed limits, provides an admin dashboard, and gives over-limit students a cleanup portal to self-service delete/download files.

What it does

  • Runs as a JupyterHub managed service (in the hub pod)
  • Polls active servers for storage usage (via jupyter-storage-usage server extension in each pod)
  • Maintains a SQLite cache of usage data (only polls active servers — typically 50–200, not all 1000)
  • Culls servers that exceed the hard limit (after a configurable grace period)
  • Pre-spawn hook swaps over-limit users to a cleanup pod (admins can always spawn normally)
  • Dashboard shows per-user storage usage, top consumers, over-limit status
  • Cleanup portal — a restricted web file manager that runs in the user's own pod:
    • Browse files and directories with recursive folder sizes (sorted by size)
    • Delete files and directories
    • Download files, or folders as zip or tar.gz
    • Shows the student's actual usage vs. their limits with a progress bar
    • No upload, no create, no edit — application-layer enforcement
    • Blocks all non-cleanup routes (/lab/, /api/, etc.) — students can't bypass it
    • Reports fresh usage to the hub after each delete so re-spawn works immediately

Architecture

┌──────────────────────────────────────────────────────────────┐
│  Hub pod                                                     │
│  ┌─────────────────┐  ┌──────────────┐  ┌────────────────┐  │
│  │ ServerPoller    │  │ Culler       │  │ Dashboard      │  │
│  │ (polls active   │  │ (stops over- │  │ + /report      │  │
│  │  servers via    │  │  limit pods  │  │  endpoint      │  │
│  │  HTTP API)      │  │  after grace)│  │                │  │
│  └────────┬────────┘  └──────┬───────┘  └───────┬────────┘  │
│           │                  │                   │           │
│           ▼                  ▼                   ▼           │
│  ┌───────────────────────────────────────────────────────┐  │
│  │  SQLite Cache (on hub PVC)  │  Pre-spawn hook         │  │
│  └───────────────────────────────────────────────────────┘  │
└──────────────────────────────────────────────────────────────┘
         │ poll /api/storage/v1/usage     ▲ report (after delete)
         ▼                               │
┌──────────────────┐           ┌──────────────────────┐
│  Normal user pod │           │  Cleanup pod          │
│  (divenb image)  │           │  (cleanupnb image)    │
│  jupyter-storage │           │  jupyter-storage-     │
│  -usage ext      │           │  usage in CLEANUP_MODE│
│  /api/storage/   │           │  =1 (file manager)    │
│  v1/usage        │           │  Blocks /lab/ routes  │
└──────────────────┘           └──────────────────────┘

The cleanup portal runs inside the user's own pod (not the hub pod), so heavy file operations (du, tar, rm on large directories) cannot affect JupyterHub. The pod has resource limits (e.g. 256 MiB RAM, 0.5 CPU) set by the spawner.

How the cleanup flow works

  1. Student exceeds the hard limit → their normal Jupyter server is culled
  2. Student clicks "Start My Server" on JupyterHub
  3. The pre-spawn hook detects they're over limit in the cache
  4. Instead of blocking, it swaps the image to the cleanup image and sets low resource limits
  5. The cleanup pod serves a file manager at /cleanup/ — all other routes (/lab/, etc.) are blocked
  6. Student browses files sorted by size, deletes large ones, downloads folders as zip
  7. After each delete, the pod reports fresh usage to the hub — cache is updated immediately
  8. Student stops the cleanup pod, clicks "Start My Server" again
  9. Pre-spawn hook sees updated cache → normal JupyterLab spawns

Quick start (z2jh)

1. Install packages

# In the hub image:
pip install jupyterhub-storage-usage

# In the singleuser image:
pip install jupyter-storage-usage

2. Build a cleanup image

A lightweight image with just jupyter-server + jupyter-storage-usage:

FROM python:3.12-slim
RUN pip install jupyter-server jupyterhub jupyter-storage-usage
# No JupyterLab — the cleanup portal replaces it

3. Add to your Helm values

hub:
  services:
    storage-usage:
      url: http://0.0.0.0:9099
      command:
        - python
        - -m
        - jupyterhub_storage_usage
      environment:
        # Required: your cleanup image
        JSU_CLEANUP_IMAGE: "registry.example.com/cleanup:0.1"
        # Required: API token for pod→hub report authentication.
        # Use the same token that JupyterHub injects into this service.
        JUPYTERHUB_API_TOKEN: "your-secret-token"
        # Optional: override defaults
        # JSU_WARN_LIMIT_BYTES: "10Gi"
        # JSU_HARD_LIMIT_BYTES: "20Gi"
        # JSU_GRACE_PERIOD_SEC: "300"

  extraConfig:
    storage-quota: |
      from jupyterhub_storage_usage import setup_storage_quota
      setup_storage_quota(c)

That's it — one env var + two lines of Python. All other settings have sensible defaults (see below).

4. (Optional) Per-user/group limit overrides

Create a ConfigMap with a JSON file:

apiVersion: v1
kind: ConfigMap
metadata:
  name: storage-limits-overrides
data:
  limits-overrides.json: |
    {
      "users": {
        "instructor1": {"warn": "50Gi", "hard": "100Gi"}
      },
      "groups": {
        "project": {"warn": "20Gi", "hard": "50Gi"}
      }
    }

Mount it and point the service at it:

hub:
  extraVolumes:
    - name: storage-limits
      configMap:
        name: storage-limits-overrides
  extraVolumeMounts:
    - name: storage-limits
      mountPath: /etc/jupyter-storage-usage
  services:
    storage-usage:
      environment:
        JSU_LIMIT_OVERRIDES_PATH: "/etc/jupyter-storage-usage/limits-overrides.json"

Limit resolution priority: user > group > global default. The overrides file is re-read on every check, so changes take effect immediately without restarting the hub.

Configuration reference

All settings are environment variables. Defaults work for a standard z2jh deployment.

Required

Env var Default Description
JSU_CLEANUP_IMAGE (none) Docker image for cleanup pods. Must be set.
JUPYTERHUB_API_TOKEN (none) API token injected into user pods so they can report usage to the hub. Must be set — use the same token JupyterHub gives this service.

Limits

Env var Default Description
JSU_WARN_LIMIT_BYTES 10Gi Soft limit — show warning in UI
JSU_HARD_LIMIT_BYTES 20Gi Hard limit — cull + cleanup pod
JSU_LIMIT_OVERRIDES_PATH (none) Path to JSON file with per-user/group overrides

Accepts K8s-style quantities: 10Gi, 500Mi, 1Ti, or plain bytes.

Polling & culling

Env var Default Description
JSU_POLL_INTERVAL_SEC 600 (10 min) How often the hub polls active servers
JSU_MAX_CONCURRENT_POLLS 50 Max concurrent HTTP polls
JSU_CULL_ON_HARD_LIMIT 1 Set 0 to disable culling
JSU_GRACE_PERIOD_SEC 300 (5 min) Time over limit before cull
JSU_ADMIN_CAN_SPAWN_OVER_LIMIT 1 Admins get normal server even over limit
JSU_EXCLUDE_USERS (none) Comma-separated usernames to skip (e.g. grader,service-account)

Cleanup pod

Env var Default Description
JSU_CLEANUP_ENABLED 1 Set 0 to disable cleanup (block spawn instead)
JSU_CLEANUP_MEM_LIMIT 256M Memory limit (KubeSpawner format: K/M/G, NOT Ki/Mi/Gi)
JSU_CLEANUP_CPU_LIMIT 0.5 CPU limit
JSU_CLEANUP_MEM_GUARANTEE 128M Memory guarantee
JSU_CLEANUP_CPU_GUARANTEE 0.1 CPU guarantee

Service

Env var Default Description
JSU_PORT 9099 Port for dashboard + report endpoint
JSU_CACHE_DB_PATH /srv/jupyterhub/storage-usage.db SQLite cache (on hub PVC in z2jh)
JSU_HUB_REPORT_URL http://hub:9099/report Where cleanup pods report usage after deletes

Cleanup portal features

Operation Allowed Notes
Browse directories ✅ Sorted by size, shows recursive folder sizes
Delete file ✅ Confirmation dialog (?confirm=yes)
Delete directory ✅ shutil.rmtree, confirmation dialog
Download file ✅ Streamed, original filename
Download folder as zip ✅ Temp file, streamed (memory-safe)
Download folder as tar.gz ✅ On-the-fly streaming
Upload file ❌ No UI for upload
Create file ❌ No UI for create
Edit file ❌ No editor
Access /lab/ ❌ Redirected to /cleanup/
Delete home root ❌ Blocked

Security

  • JupyterHub proxy authentication: the cleanup portal is only reachable through the hub proxy, which authenticates users via OAuth2
  • Path traversal protection: _resolve_safe() uses os.path.realpath() + prefix check
  • User isolation: each student can only access their own PVC mount
  • XSRF disabled for cleanup endpoints: the plain HTML forms don't carry XSRF tokens; the hub proxy provides authentication instead
  • Catch-all route blocking: in cleanup mode, ALL non-cleanup routes redirect to /cleanup/ — students cannot access /lab/, /api/, /terminals/, etc.

Install

pip install jupyterhub-storage-usage  # in the hub image
pip install jupyter-storage-usage     # in the singleuser image

License

MIT

Metadata

Release files for jupyterhub-storage-usage 0.1.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for jupyterhub-storage-usage 0.1.1
File Size Uploaded
jupyterhub_storage_usage-0.1.1.tar.gz 23.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for jupyterhub-storage-usage 0.1.1
File Interpreter ABI Platform
jupyterhub_storage_usage-0.1.1-py3-none-any.whl Python 3 none any Details

Total release size: 54.4 kB

Release files / jupyterhub_storage_usage-0.1.1.tar.gz

Download URL jupyterhub_storage_usage-0.1.1.tar.gz
Size 23.7 kB
Tags Source
SHA-256 checksum
How to use checksums
e3ec202bfd22f00fb50c705fc61fe70fded99d43dad7084db4e3cebd9a4200d6
BLAKE2b-256 checksum
How to use checksums
d3d8946a5009e04a61669340a9b50c4ce07ce369ccb1c5bc546e0b0e4381c155
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.5

Release files / jupyterhub_storage_usage-0.1.1-py3-none-any.whl

Download URL jupyterhub_storage_usage-0.1.1-py3-none-any.whl
Size 30.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
501b61ae59a72a8e049357521c3a175895db3ec80ced9436f310b876403305dd
BLAKE2b-256 checksum
How to use checksums
faa04cd7911017d3da0a692c40beb8780fd990b71f7174b0c57209f8f62fbb9e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.5

Release history Release notifications | RSS feed

This release

0.1.1 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page