Introduction
High Performance with Caddy and FrankenPHP
title: High Performance with Caddy and FrankenPHP description: Learn how Kipchak leverages FrankenPHP for high performance.
Kipchak is built with performance in mind. One of its key features is its native support for FrankenPHP in worker mode, running on the Caddy web server.
What is Caddy?
Caddy is a powerful, enterprise-ready web server with automatic HTTPS. It's written in Go and designed to be simple to configure and use. Caddy serves as the foundation for FrankenPHP, providing:
- Automatic HTTPS: Built-in SSL/TLS certificate management with Let's Encrypt
- HTTP/2 and HTTP/3: Modern protocol support out of the box
- Reverse Proxy: Advanced load balancing and proxy capabilities
- Static File Serving: Efficient static asset delivery with automatic compression
- Extensibility: Plugin architecture for custom functionality
NOTE: In the docker container the HTTPS features of Caddy are disabled by default.
If you run on Kubernetes, it's likely that you will be terminating TLS at the ingress controller.
What is FrankenPHP?
FrankenPHP is a modern PHP application server built on top of Caddy. It embeds the PHP interpreter directly into the web server, enabling:
- Worker Mode: Keep your PHP application in memory across requests
- Early Hints: HTTP 103 support for faster page loads
- Real-time: Built-in support for Server-Sent Events (SSE) and Mercure
- Production Ready: Battle-tested in high-traffic applications
FrankenPHP Worker Mode
FrankenPHP's worker mode is a game-changer for PHP performance. Unlike traditional PHP-FPM, where each request boots up the entire application framework, worker mode boots your application once and keeps it in memory, handling multiple requests without restarting.
How Worker Mode Works
- Boot Once: Your application (including Kipchak, dependencies, and configuration) loads once on startup
- Handle Requests: Each incoming HTTP request is passed to the in-memory application
- Reset State: After each request, the worker resets to a clean state
- Reuse Resources: Database connections, compiled templates, and other resources persist between requests
Benefits of Worker Mode
- 10-20x Faster: No boot-up time for each request dramatically reduces latency
- Shared Resources: Database connections, caches, and other resources can be shared across requests
- High Throughput: Handle thousands of requests per second on modest hardware
- Lower Memory: Fewer processes needed compared to traditional FPM
- Reduced CPU: No repeated framework initialization on every request
Debugging in Worker Mode
Worker mode does not allow you to echo or print to screen. This output goes to stdout and is not visible in the browser. You will also not be able to use the exist command after a var_dump() as that kills the worker process.
If you would like to use this way of working (rather than using xdebug, for instance), you can either us the dump() function packaged with Kipchak or run FrankenPHP in classic mode like FPM/Apache.
The dump function works in contollers and needs to be prefaced with a return statement.
You can use it like this in a Controller:
use function Kipchak\Core\dump;
return dump('$var');
Configuring FrankenPHP and Caddy
Kipchak comes pre-configured to work with FrankenPHP. The Caddyfile included in the starter project is set up to run the application in worker mode:
# The Caddyfile is an easy way to configure FrankenPHP and the Caddy web server.
#
# https://frankenphp.dev/docs/config
# https://caddyserver.com/docs/caddyfile
{
skip_install_trust
{$CADDY_GLOBAL_OPTIONS}
metrics
admin :2019
frankenphp {
{$FRANKENPHP_CONFIG}
num_threads 2 # Sets the number of PHP threads to start. Default: 2x the number of available CPUs.
max_threads 4 # Limits the number of additional PHP threads that can be started at runtime. Default: num_threads. Can be set to 'auto'.
max_wait_time 60s # Sets the maximum time a request may wait for a free PHP thread before timing out. Default: disabled.
# php_ini <key> <value> # Set a php.ini directive. Can be used several times to set multiple directives.
worker {
file ./index.php # Sets the path to the worker script.
num 1 # Sets the number of PHP threads to start, defaults to 2x the number of available CPUs.
# env <key> <value> # Sets an extra environment variable to the given value. Can be specified more than once for multiple environment variables.
# watch <path> # Sets the path to watch for file changes. Can be specified more than once for multiple paths.
name kipchak # Sets the name of the worker, used in logs and metrics. Default: absolute path of worker file
max_consecutive_failures 7 # Sets the maximum number of consecutive failures before the worker is considered unhealthy, -1 means the worker will always restart. Default: 6.
}
}
}
{$CADDY_EXTRA_CONFIG}
:80 {
#{$SERVER_NAME:localhost} {
#log {
# # Redact the authorization query parameter that can be set by Mercure
# format filter {
# request>uri query {
# replace authorization REDACTED
# }
# }
#}
root {$SERVER_ROOT}
encode
# Uncomment the following lines to enable Mercure and Vulcain modules
#mercure {
# # Transport to use (default to Bolt)
# transport_url {$MERCURE_TRANSPORT_URL:bolt:///data/mercure.db}
# # Publisher JWT key
# publisher_jwt {env.MERCURE_PUBLISHER_JWT_KEY} {env.MERCURE_PUBLISHER_JWT_ALG}
# # Subscriber JWT key
# subscriber_jwt {env.MERCURE_SUBSCRIBER_JWT_KEY} {env.MERCURE_SUBSCRIBER_JWT_ALG}
# # Allow anonymous subscribers (double-check that it's what you want)
# anonymous
# # Enable the subscription API (double-check that it's what you want)
# subscriptions
# # Extra directives
# {$MERCURE_EXTRA_DIRECTIVES}
#}
#vulcain
{$CADDY_SERVER_EXTRA_DIRECTIVES}
php_server {
}
}
# As an alternative to editing the above site block, you can add your own site
# block files in the Caddyfile.d directory, and they will be included as long
# as they use the .caddyfile extension.
import Caddyfile.d/*.caddyfile
Understanding FrankenPHP Configuration Options
Let's break down the key FrankenPHP configuration directives and when to adjust them:
Global FrankenPHP Settings
num_threads
num_threads 2
Purpose: Sets the number of PHP threads that FrankenPHP starts initially.
Default: 2x the number of available CPU cores
When to adjust:
- Increase for CPU-bound applications that can utilize more threads
- Decrease to reduce memory consumption on resource-constrained systems
- Rule of thumb: Start with 1-2x your CPU cores and adjust based on monitoring
max_threads
max_threads 4
Purpose: Limits the maximum number of additional PHP threads that can be started at runtime.
Default: Same as num_threads, or set to auto for dynamic scaling
When to adjust:
- Set higher than
num_threadsto allow dynamic scaling under load - Set to
autoto let FrankenPHP manage thread scaling automatically - Important: More threads = more memory usage. Monitor your system's memory capacity
max_wait_time
max_wait_time 60s
Purpose: Maximum time a request waits for a free PHP thread before timing out.
Default: Disabled (requests wait indefinitely)
When to adjust:
- Enable in production to prevent requests from hanging forever
- Set to
30s-60sfor typical web applications - Set lower (10s-20s) for API endpoints that should fail fast
- Set higher (120s+) for applications with long-running operations
Why it matters: Without this, during traffic spikes, requests can queue indefinitely. With it set, users get a clear timeout error instead of an indefinite hang.
Worker-Specific Settings
worker.file
file ./index.php
Purpose: Path to the worker script that boots your application.
Default: None (required)
Note: This should point to your application's entry point. For Kipchak, this is index.php.
worker.num
num 1
Purpose: Number of worker processes to start.
Default: 2x the number of available CPU cores
When to adjust:
- Increase for high-traffic applications (2-4 workers per CPU core)
- Decrease for applications with high memory usage per worker
- Each worker maintains its own PHP application instance in memory
- Balance: More workers = better concurrency but higher memory usage
Example scenarios:
- Low traffic, 4 CPU cores:
num 2(conserve memory) - High traffic, 4 CPU cores:
num 8-16(maximize throughput) - Memory-intensive app:
num 2-4(prevent memory exhaustion)
worker.name
name kipchak
Purpose: Identifies the worker in logs and metrics.
Default: Absolute path of the worker file
When to adjust: Use descriptive names when running multiple workers or applications.
worker.max_consecutive_failures
max_consecutive_failures 7
Purpose: Maximum number of consecutive crashes before the worker is marked unhealthy.
Default: 6
When to adjust:
- Increase (10-20) during development when debugging crashes
- Decrease (3-5) in production to fail fast on persistent issues
- Set to
-1to always restart the worker (not recommended for production) - Set to
0to never restart after any failure
Why it matters: This prevents a buggy deployment from repeatedly crashing and restarting. After hitting the limit, FrankenPHP stops trying to restart that worker, allowing you to investigate without consuming resources on repeated crashes.
Example scenarios:
- Development:
max_consecutive_failures 20(be forgiving during development) - Production (stable):
max_consecutive_failures 5(fail fast if something's wrong) - Production (experimental):
max_consecutive_failures 10(allow some recovery attempts)
worker.watch
watch /path/to/src
watch /path/to/config
Purpose: Automatically restarts workers when files in the specified paths change.
Default: None (disabled)
When to use:
- Development only: Enables hot-reloading during development
- Never in production: Adds overhead and can cause issues
- Specify multiple paths to watch different directories
worker.env
env APP_ENV production
env DEBUG false
Purpose: Sets environment variables specifically for the worker process.
Default: Inherits from the system environment
When to use: Override specific environment variables for the worker without affecting the entire system.
Caddy Server Configuration
Admin Interface
admin :2019
Enables Caddy's admin API on port 2019 for runtime configuration changes and metrics.
Metrics
metrics
Enables Prometheus-compatible metrics at /metrics endpoint on the admin interface.
Encoding
encode
Enables automatic compression (gzip, zstd, brotli) for responses if the request contains an Accept-Encoding header.
Performance Tuning Tips
For High Traffic
frankenphp {
num_threads 4
max_threads 8
max_wait_time 30s
worker {
file ./index.php
num 8
max_consecutive_failures 5
}
}
For Memory-Constrained Systems
frankenphp {
num_threads 2
max_threads 2
max_wait_time 45s
worker {
file ./index.php
num 2
max_consecutive_failures 7
}
}
For Development
frankenphp {
num_threads 1
max_threads 2
worker {
file ./index.php
num 1
watch ./src
watch ./config
max_consecutive_failures 20
}
}
Monitoring Worker Health
FrankenPHP exposes metrics through Caddy's admin interface. Access metrics at:
http://localhost:2019/metrics
Key metrics to monitor:
frankenphp_workers_total: Total number of workersfrankenphp_workers_failed_total: Number of failed workersfrankenphp_requests_duration_seconds: Request duration histogramfrankenphp_requests_total: Total requests handled
Running FrankenPHP / Caddy in Classic Mode
The starter project comes with what you need to run FrankenPHP and Caddy in classic mode.
- It contains
index.nonworker.phpin the/var/www/htmldirectory, which is a non-worker mode entry point for FrankenPHP. - It contains
Caddyfile.nonworkerin the/etc/caddydirectory that runs Caddy in classic mode.
To enable classic mode, rename the existing index.php file to index.worker.php and the index.nonworker.php file to index.php in /var/www/html. Then, change your Dockerfile to add the right Caddyfile into the container. The changed file will look like this:
FROM 1x.ax/mamluk/php/8.4:franken-dev
# Copy files
COPY . /var/www/
#######
####### COMMENT out this Caddyfile
#######
# Leave the below line as it is to run FrankenPHP in Worker Mode
# COPY etc/caddy/Caddyfile /etc/frankenphp/
#######
####### UNCOMMENT this Caddyfile
#######
# If you do not want FrankenPHP to run in worker mode, uncomment the line below and comment line 7.
COPY etc/caddy/Caddyfile.nonworker /etc/frankenphp/Caddyfile
# Run Composer
RUN export COMPOSER_ALLOW_SUPERUSER=1 && cd /var/www && composer install --no-dev
# Delete stuff we do not need
RUN rm -rf /var/www/.git
RUN rm -rf /var/www/.gitignore
Now when you run docker-compose up --build --watch, FrankenPHP and Caddy will run in classic mode.