Configuration
Folk is configured via folk.toml in your project root. All settings have sensible defaults — you only need to specify what you want to change.
Environment variables with the FOLK_ prefix override file settings. Use double underscore (__) to separate the section name from the field name:
FOLK_WORKERS__COUNT=8
FOLK_HTTP__LISTEN=0.0.0.0:9000
FOLK_LOG__FILTER=debug
FOLK_SERVER__SHUTDOWN_TIMEOUT=60s
FOLK_WORKERS__MAX_JOBS=500
The pattern is FOLK_{SECTION}__{FIELD} — single underscore after FOLK, double underscore between section and field. This allows field names that contain underscores (e.g. max_jobs, shutdown_timeout) to work correctly.
Server
[server]
rpc_socket = "/tmp/folk.sock" # Admin RPC Unix socket path
shutdown_timeout = "30s" # Graceful shutdown timeout
Workers
[workers]
script = "vendor/bin/folk-worker" # PHP worker script
php = "php" # PHP binary path
count = 4 # Worker processes the master forks (NTS)
max_jobs = 1000 # Recycle after N requests (0 = never)
ttl = "3600s" # Recycle after this lifetime
exec_timeout = "30s" # Per-request HARD deadline (watchdog kills + respawns)
max_memory_mb = 256 # Recycle a worker over this RSS (omit = disabled)
boot_timeout = "30s" # Worker boot timeout
destroy_timeout = "10s" # SIGTERM → wait → SIGKILL a worker that won't recycle
liveness_timeout = "0s" # Force-recycle a worker whose runtime stalls (0 = off)
warmup = true # Opcache warmup before worker spawn
liveness_timeout (runtime liveness)
exec_timeout only catches a request that runs too long. It can't catch a worker whose async runtime has wedged outside a request (e.g. a deadlocked runtime whose HTTP listener stopped accepting). With liveness_timeout set, each worker's runtime emits a heartbeat every second (independent of PHP and of traffic); if it stalls past the timeout while the process is alive, the master force-recycles the worker. Because the heartbeat is traffic-independent, an idle worker is never mistaken for a hung one — leave it 0 (off) or set it generously (tens of seconds). Per-worker folk_worker_* metrics (see the metrics plugin) expose the same signal for alerting.
max_jobs / ttl in the fork model
Under the fork-after-warm model these are enforced by the master. Both are on by default (max_jobs = 1000, ttl = 3600s), so workers recycle after 1000 requests or one hour. Set max_jobs = 0 and a large ttl if you want long-lived workers and rely only on max_memory_mb. When many workers cross a limit together the master staggers recycles (one per second) to avoid a cold-start stampede.
Opcache warmup
When warmup is enabled (default), Folk automatically compiles all files from vendor/composer/autoload_classmap.php into shared opcache before spawning workers. This eliminates the parse+compile overhead on first requests — workers start with hot opcache immediately. Works with any framework or vanilla PHP project that uses Composer. Requires composer install --optimize-autoloader for full coverage.
Worker recycling
Workers are recycled (terminated and respawned) when they exceed max_jobs or ttl. This prevents memory leaks from accumulating. The main thread worker is never recycled.
destroy_timeout
When a worker is recycled (over max_memory_mb) the master sends it SIGTERM and lets in-flight requests drain. If the worker is wedged in a C call and never exits, destroy_timeout (default 10s) bounds the wait: the master then escalates to SIGKILL of the worker's whole process group, so the pool can't silently shrink. See Background processes below for what "process group" means here.
Background processes
A Folk worker is a long-lived process — unlike PHP-FPM, it is not recreated per request. That changes how background work behaves:
- Synchronous
exec()/system()/shell_exec()/proc_open()+proc_close()— fully supported. PHP blocks and reaps the child itself, exactly as anywhere else. - Detached processes (
exec("cmd &"),nohup, a self-daemonizingproc_openwithoutproc_close) — discouraged. Each worker leads its own process group, so such a child is killed together with the worker on force-kill/recycle and cannot leak across restarts. It will not survive its worker, so don't use this pattern for durable background work. pcntl_fork()from a request — not supported. Forking duplicates the worker's multithreaded runtime; the child inherits broken locks and would hang and leak PIDs. Folk guards against the hang (the fork child is force-exited), but you must not rely on forking from a request.
For background jobs, scheduled tasks or parallel work, use the jobs plugin (queues, retries, delays) or the process plugin (supervised long-running processes with restart policies) instead of spawning from a request. As an extra safety net, run the container with an init/reaper as PID 1 (e.g. docker run --init or tini).
Logging
[log]
filter = "info" # "debug", "info", "warn", "error"
format = "text" # "text", "json", or "pretty"
[log.plugins] # Per-plugin log level overrides
http = "warn" # Only warnings and errors from HTTP plugin
jobs = "debug" # Verbose logging from jobs plugin
core = "info" # Core server logging
Per-Plugin Log Levels
The [log.plugins] section lets you set per-plugin log levels using friendly names. No need to know Rust crate names — Folk maps them automatically:
| Config key | Rust target |
|---|---|
http |
folk_plugin_http |
jobs |
folk_plugin_jobs |
grpc |
folk_plugin_grpc |
metrics |
folk_plugin_metrics |
process |
folk_plugin_process |
core |
folk_core |
ext |
folk_ext |
All output goes to stdout. The three formats share the same data structure (timestamp, level, target, message, context fields) — only the visual representation differs.
text — compact, one line per event:
2026-05-20T14:30:00Z INFO [http] 200 GET /api/users 12ms
2026-05-20T14:30:01Z WARN [process] restarting name=scheduler restarts=2
json — structured, for ELK/Loki/Grafana:
{"ts":"2026-05-20T14:30:00Z","level":"INFO","plugin":"http","msg":"200 GET /api/users","status":200,"duration_ms":12}
pretty — multi-line, for debugging:
Note
The RUST_LOG environment variable takes precedence over filter if set.
Request correlation (request_id)
Every request carries a globally-unique request_id — a UUID v7 (time-ordered,
so it sorts by creation time, and unique across instances and restarts). It is a
single correlation key you can use across aggregated logs (Loki, ELK, Grafana)
without also filtering by host or pod. From PHP, read it with
\Folk\Sdk\Folk::requestId() (returns "" outside a request or without the
extension).
The same id appears in two places automatically:
- HTTP access log — the
request_idfield of each access-log line (enable with[http] access_log = true), so the Rust-side line and the PHP application log of the same request share one id. - Application logs via the framework adapters:
- folk/laravel (≥ 0.3.5) —
request_idis added to theextraof every record on the default log channel, soLog::info(...)lines carry it. - folk/symfony (≥ 0.1.2) — when the logger is Monolog (e.g. with
symfony/monolog-bundle),request_idis added to theextraof records on the main channel. Without Monolog it is a no-op.
The id is read when each record is written, so it never leaks between requests on a recycled worker and is simply absent when there is no request in flight.
Plugins
Each plugin has its own configuration section. See the plugin pages for details:
Section present = plugin enabled
A compiled-in plugin is loaded only when its config section is present in folk.toml.
No section means the plugin is not instantiated — it does not start and writes nothing to
the log. An empty section enables the plugin with its defaults:
This lets you compile one binary with every plugin and switch each on or off purely through
config. A config with only [http] runs an HTTP-only server; jobs, gRPC, metrics and process
stay silent. There is no enabled = false flag — simply omit the section to disable a plugin.
Omitting [http] is allowed too (e.g. a jobs-only or gRPC-only server); the server then
starts without an HTTP listener and does so silently.
Duration Format
Duration fields accept human-readable values:
| Format | Meaning |
|---|---|
30s |
30 seconds |
5m |
5 minutes |
1h |
1 hour |
1d |
1 day |
Complete Example
[server]
shutdown_timeout = "10s"
[workers]
script = "vendor/bin/folk-worker"
count = 4
max_jobs = 1000
[log]
filter = "info"
format = "json"
[log.plugins]
http = "warn"
process = "debug"
[http]
listen = "0.0.0.0:8080"
access_log = true
public_dir = "public" # serve static files from disk before dispatching to PHP
[jobs]
driver = "redis"
redis_url = "redis://127.0.0.1:6379"
[[jobs.queues]]
name = "default"
concurrency = 4
max_retries = 3
[grpc]
listen = "0.0.0.0:50051"
proto = ["proto/service.proto"]
max_recv_message_size = "4mb"
timeout = "30s"
[metrics]
listen = "0.0.0.0:9090"
prefix = "folk"
ready_path = "/ready"
[[metrics.collectors]]
name = "app_requests_total"
type = "counter"
help = "Total application requests"
labels = ["method", "endpoint"]
[[process.processes]]
name = "scheduler"
command = "php artisan schedule:work"
restart = "always"
See the Configuration Reference for a fully commented folk.toml with all options.