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
- Very specific: line 55
Never use --reload in production.
- Very specific: line 112
Unbounded caches: use functools.lru_cache(maxsize=N) not {} for in-process caching.
- Distinctive: line 128
Use environment variables for all configuration.
- Distinctive: line 173
No graceful timeout configured: workers killed mid-request during deploys
≈ “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.
Files
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
| WSGI | ASGI | |
| Frameworks | Django, Flask, Falcon | FastAPI, Starlette, Django 4+ (async views) |
| Server | gunicorn | uvicorn, hypercorn, daphne |
| Concurrency model | One request per worker | Many requests per worker (async) |
| I/O concurrency | Via multiple workers | Via 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:8000For 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 8000Or 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:8000Never 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 finishFor 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
weakreffor 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 automaticallypydantic-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
--reloadflag 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')returningNonesilently instead of failing fast - Single worker in production (no concurrency, single point of failure)
lru_cachewithoutmaxsize(unbounded, will grow forever)
In this repository25 other files it gives its agents.
- api-designSkill
- arch-reviewSkill
- db-reviewSkill
- debugSkill
- dotnet-asyncSkill
- dotnet-opsSkill
- dotnet-testingSkill
- dotnet-typesSkill
- go-concurrencySkill
- go-opsSkill
- go-testingSkill
- go-typesSkill
- incidentSkill
- node-asyncSkill
- node-opsSkill
- perfSkill
- pr-prepSkill
- py-asyncSkill
- py-testingSkill
- py-typesSkill
- refactorSkill
- securitySkill
- test-genSkill
- ts-testingSkill
- ts-typesSkill
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.

