This page covers how Frappe is layered (bench → app → site), the technology stack
it runs on, and what happens when a request comes in. Understanding these three
helps you reason about where your code runs and where data lives.
The stack
A Frappe deployment is made of a few cooperating processes:
| Layer | Technology | Role |
|---|---|---|
| Web / API server | Python (Werkzeug WSGI), served by Gunicorn in production | Handles HTTP requests, runs controllers |
| Realtime | Node.js (Socket.IO) | WebSocket push for live updates |
| Database | MariaDB or PostgreSQL | Persistent storage; each site is a separate database |
| Cache & queue | Redis | Caching, background job queue, pub/sub |
| Workers & scheduler | Python (RQ) | Run background jobs and scheduled tasks |
| Asset build | Node.js (esbuild) | Bundle JS/CSS for the Desk and websites |
In development these run together under bench start. In production they are
managed by a process supervisor and a web server / reverse proxy.
Bench, app, and site
Frappe separates code from data through three nested concepts.
Bench
A bench is a working directory that holds a Python virtual environment, the
Node toolchain, configuration, and an apps/ and sites/ folder. It is your
development/deployment environment and is created and managed by the
Bench CLI (a separate tool, installed
separately from the framework).
frappe-bench/
├── apps/ # source code of installed apps
│ ├── frappe/
│ └── your_app/
├── sites/ # one folder per site + shared config
│ ├── common_site_config.json
│ └── mysite.localhost/
├── config/ # supervisor, nginx, redis configs
└── env/ # python virtualenv
App
An app is an installable Python package living in apps/. frappe itself is
an app; your code is another app that depends on it. An app contributes DocTypes,
controllers, hooks, pages, and assets. The same app can be
installed on many sites. See Modules & App Structure.
Site
A site is an isolated instance with its own database, files, and config,
addressed by a hostname (for example mysite.localhost). One bench can host many
sites (multitenancy), and each site has a chosen set of apps installed on it.
bench (environment)
├── apps (code, shared across sites)
└── sites (data; each site picks which apps it installs)
Apps and sites are parallel: apps hold code, sites hold data. A site does not
live inside an app. Instead, a site records which of the bench's apps are
installed on it.
This split is why the same your_app codebase can serve many independent
customers from one bench.
Where your code runs
- Python runs on the server: controllers, API methods, jobs, scheduled tasks.
- JavaScript runs in the browser: Desk form scripts,
list-view customizations, and any custom front-end talking to the REST API. - Jinja runs on the server to render website/portal pages and print formats.
Execution contexts
Server-side Python does not only run during web requests. Each context sets up a
site (frappe.init() + frappe.connect()), runs your code against
frappe.local and a database transaction, then tears the context down with
frappe.destroy(). They differ in how they are triggered.
- Web request: a Gunicorn/Werkzeug worker handles one HTTP request, as in the
lifecycle above.frappe.init(..., is_request=True). - Background job: an RQ worker pulls a job off the Redis queue and runs it
withfrappe.init(..., is_job=True), often as a specific user. See
background jobs. - Scheduler: a periodic process enqueues scheduled events, which then run as
background jobs. - Patches: data and schema migrations run once during
bench migrate, in a
one-off script context rather than per request. - CLI / console:
bench executeandbench consoleinit a site and run code
directly, useful for scripts and debugging.
Because each context manages its own transaction, you usually do not call
frappe.db.commit() yourself. The request and job runners commit on success and
roll back on error.
Request lifecycle
When an HTTP request hits the Frappe web server, it flows roughly like this:
- WSGI entry: the request enters the Werkzeug/WSGI application
(frappe/app.py). - Init:
init_request()resolves the site from the host (or the
X-Frappe-Site-Nameheader), callsfrappe.init()andfrappe.connect()to
open a database connection and request-local context (frappe.local), and
runsbefore_requesthooks. - Auth:
validate_auth()resolves the session or API key into
frappe.session.user. - Routing: the path is dispatched.
/api/v2/method/<dotted.path>→ a whitelisted method/api/v2/document/<DocType>→ the REST API for documents/app/...→ the Desk (rewritten to and served under/desk)- any other path → the website/portal renderer (Jinja)
- Permissions: role and document permissions are enforced.
- Controller logic: the request runs your Python code. This is a Document's
lifecycle hooks (validate,on_update, and so on)
or a custom method. - Response: the result is serialized (JSON or HTML), the transaction is
committed,after_requesthooks run, and the context is torn down with
frappe.destroy().
Slow or deferrable work (emails, exports, syncs) is pushed to Redis-backed
background jobs instead of blocking the response.
See also
- Key Concepts: the vocabulary above, defined
- Hooks: how apps plug into the framework
- Bench Overview: managing the environment