Skip to content

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-daemonizing proc_open without proc_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 requestnot 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:

  2026-05-20T14:30:00Z INFO [http]
    200 GET /api/users
    status: 200
    duration_ms: 12

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_id field 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_id is added to the extra of every record on the default log channel, so Log::info(...) lines carry it.
  • folk/symfony (≥ 0.1.2) — when the logger is Monolog (e.g. with symfony/monolog-bundle), request_id is added to the extra of 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:

[metrics]   # empty section — metrics enabled with default settings

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.