agilentics / boiler
from os import environ

bind = ":" + environ.get("PORT", "8080")

# Sized by RAM, not CPU. The usual cpu_count()*2+1 rule assumes a dedicated box;
# on a memory-capped App Engine instance it is actively wrong - the sandbox
# reports 2 CPUs, so that formula forks 5 sync workers, each a full copy of the
# app. On an F1 (384 MiB) that overran the hard limit and the instance was killed
# at boot, reporting "0 requests serviced" and nothing else.
#
# Two fits F1 comfortably. WEB_CONCURRENCY tunes it if the instance class changes;
# measure the per-worker footprint before raising it.
workers = int(environ.get("WEB_CONCURRENCY", "2"))

# Threads, not processes, for the second axis of concurrency. Almost all of a
# request in a database-backed app is spent waiting on the database, not burning
# CPU, and a sync worker holds its whole process idle for that wait - so two
# workers would mean two concurrent requests for the entire app, and one slow
# read blocks half of it while later requests queue behind it (visible as
# multi-second pendingTime on endpoints that themselves take under a second).
#
# Threads share the process, so this costs memory only in stacks - the RAM limit
# that caps `workers` is untouched, and concurrency goes from 2 to 8 per instance.
#
# Safe only while nothing in the app holds mutable process state across a request.
# That holds here: the session is a scoped_session (thread-local) removed on
# app-context teardown. If you add a module-level mutable cache, revisit this.
# The DB pool is sized against this number in utils/db_wrapper.
threads = int(environ.get("WEB_THREADS", "4"))
worker_class = "gthread"

errorlog = "-"
accesslog = "-"
loglevel = "info"
timeout = 120