agentleFS
AllThingsSmittyAllThingsSmitty/proven-skills/skills/py-ops/SKILL.md

py-ops

Skill1 starsChanged 27 days agoMITView on GitHub

Production Python operations advisor. Always use this skill when deploying or operating Python web apps, choosing between WSGI and ASGI, configuring gunicorn or uvicorn, handling graceful shutdown, SIGTERM, config validation, or health checks. Use when the user asks "gunicorn vs uvicorn", "how many workers should I use", "how do I handle SIGTERM in Python", "graceful shutdown in Django or FastAPI", "validate env vars at startup", "my Python app leaks memory", or "WSGI vs ASGI". Read this before advising on any Python production or deployment question.

We split this skill into the rules it gives an agent

4 rules. About 0 are common, 2 are distinctive, and 2 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈2

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 2

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Never use --reload in production.

    line 55
  2. Very specific:

    Unbounded caches: use functools.lru_cache(maxsize=N) not {} for in-process caching.

    line 112
  3. Distinctive:

    Use environment variables for all configuration.

    line 128
  4. Distinctive:

    No graceful timeout configured: workers killed mid-request during deploys

    line 173

≈ “Common” is an estimate: at least 1 in 5 of the 308,232 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Production Python Operations

Python web apps have two distinct runtime models, synchronous (WSGI) and asynchronous (ASGI), with different servers, worker models, and operational characteristics. Mixing them up or misconfiguring workers is the most common source of Python production problems.

WSGI vs ASGI

WSGIASGI
FrameworksDjango, Flask, FalconFastAPI, Starlette, Django 4+ (async views)
Servergunicornuvicorn, hypercorn, daphne
Concurrency modelOne request per workerMany requests per worker (async)
I/O concurrencyVia multiple workersVia event loop within each worker

WSGI handles one request per worker at a time. Scale by adding workers. Blocking I/O is fine: each worker blocks independently.

ASGI handles many requests per worker concurrently via the event loop. Blocking I/O kills concurrency (see py-async). Fewer workers needed, but each worker must be async-safe.

Gunicorn worker sizing (WSGI)

The classic formula: (2 × CPU cores) + 1.

gunicorn myapp.wsgi:application \
  --workers 5 \          # 2 × 2 cores + 1
  --worker-class sync \  # default; gthread for threaded
  --timeout 30 \
  --bind 0.0.0.0:8000

For WSGI apps with heavy I/O (database calls, external APIs), use --worker-class gthread with --threads to get concurrency within each worker without going fully async.

Uvicorn for ASGI

uvicorn myapp.main:app \
  --workers 4 \
  --host 0.0.0.0 \
  --port 8000

Or run gunicorn with uvicorn's worker class (recommended for production; gunicorn handles process management, uvicorn handles async):

gunicorn myapp.main:app \
  --workers 4 \
  --worker-class uvicorn.workers.UvicornWorker \
  --bind 0.0.0.0:8000

Never use --reload in production. It watches the filesystem and restarts workers on every file change, which is a serious performance and security problem in production.

Graceful shutdown

Gunicorn handles SIGTERM gracefully by default: it finishes in-flight requests before exiting. Configure the timeout:

gunicorn --graceful-timeout 30  # wait up to 30s for workers to finish

For custom shutdown logic in your app (closing database connections, flushing queues):

import signal
import sys

def handle_sigterm(signum, frame):
    # flush pending work, close connections
    cleanup()
    sys.exit(0)

signal.signal(signal.SIGTERM, handle_sigterm)

In a containerized environment, also handle SIGINT (Ctrl+C / docker stop):

signal.signal(signal.SIGINT, handle_sigterm)

Health checks

Separate liveness from readiness:

# FastAPI example
@app.get('/health/live')
async def liveness():
    return {'status': 'ok'}  # just confirms the process is running

@app.get('/health/ready')
async def readiness():
    try:
        await db.execute('SELECT 1')
        return {'status': 'ok'}
    except Exception:
        raise HTTPException(status_code=503, detail='Database unavailable')

The readiness check should fail during startup (before the DB pool is ready) and during shutdown. Liveness should only fail if the process itself is broken, not if a dependency is down.

Memory management

Python's garbage collector handles most memory automatically, but leaks happen:

  • Circular references: Python's GC handles these, but they delay collection. Use weakref for back-references (parent → child → parent patterns).
  • Large objects in module scope: loaded once at import time and never freed. Load lazily or on demand.
  • Unbounded caches: use functools.lru_cache(maxsize=N) not {} for in-process caching.
  • Generator vs list: for large sequences, use generators (yield) to avoid loading everything into memory at once.

Diagnose with tracemalloc (stdlib) for snapshots or memory-profiler for line-by-line profiling:

import tracemalloc
tracemalloc.start()
# ... run the suspected code ...
snapshot = tracemalloc.take_snapshot()
for stat in snapshot.statistics('lineno')[:10]:
    print(stat)

Environment configuration

Use environment variables for all configuration. Fail at startup if required config is missing:

# pydantic-settings (recommended)
from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    database_url: str         # required — raises at startup if missing
    secret_key: str
    debug: bool = False
    max_connections: int = 10

settings = Settings()         # reads from env vars automatically

pydantic-settings validates types, provides defaults, and raises a clear error on startup for missing required values, not on the first request that needs them.

Logging for production

Configure structured logging at startup, not scattered throughout the codebase:

import logging
import json

class JSONFormatter(logging.Formatter):
    def format(self, record):
        return json.dumps({
            'level': record.levelname,
            'message': record.getMessage(),
            'logger': record.name,
        })

handler = logging.StreamHandler()
handler.setFormatter(JSONFormatter())
logging.root.addHandler(handler)
logging.root.setLevel(logging.INFO)

Or use structlog for a production-ready structured logging setup with less boilerplate.

What to watch for in code review

  • --reload flag in production scripts or Dockerfiles
  • ASGI framework (FastAPI/Starlette) deployed with a WSGI server (plain gunicorn without UvicornWorker)
  • No graceful timeout configured: workers killed mid-request during deploys
  • Required config accessed with os.environ.get('KEY') returning None silently instead of failing fast
  • Single worker in production (no concurrency, single point of failure)
  • lru_cache without maxsize (unbounded, will grow forever)

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

Reviews can't be read right now.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.