Skip to content

Conversation

@michalsn
Copy link
Member

Description
This PR adds experimental support for FrankenPHP Worker Mode, allowing to handle multiple HTTP requests within the same PHP process. Instead of bootstrapping the framework for every request (traditional PHP-FPM model), the worker boots once and reuses resources across requests, resulting in 2-3x performance improvements for typical database-driven applications.

How it works

  1. One-time bootstrap: Framework loads autoloader, configuration, services, and routes once at worker startup
  2. Request loop: Each incoming request reuses existing resources, resets only request-specific state, processes the request, and cleans up
  3. Connection persistence: Database and cache connections are validated and reused across requests
  4. State isolation: Services, factories, and superglobals are properly reset between requests to prevent data leakage

New features

  • app/Config/WorkerMode.php - Configuration for persistent services and garbage collection
  • php spark worker:install - Generates Caddyfile and worker entry point
  • php spark worker:uninstall - Removes worker mode files

Optimizations

Database:

  • Added BaseConnection::ping() method for connection health checks
  • Postgre uses native pg_ping(), other drivers use SELECT 1
  • DatabaseConfig::validateForWorkerMode() - validates connections at request start
  • DatabaseConfig::cleanupForWorkerMode() - detects uncommitted transactions and rolls back

Session handlers:

  • Added PersistsConnection trait for Redis and Memcached handlers
  • Connections are pooled by configuration hash and reused across requests
  • Connections are validated before reuse, automatically re-established if unhealthy

Services and state management:

  • Boot::bootWorker() - one-time initialization for worker mode
  • CodeIgniter::resetForWorkerMode() - resets request-specific state
  • Services::resetForWorkerMode() - resets non-persistent services between requests
  • Services::validateForWorkerMode() - validates cache connections
  • Events::cleanupForWorkerMode() - clears event performance data

Debug Toolbar:

  • Toolbar::reset() - resets collectors between requests (development only)

No impact on traditional PHP-FPM
All worker mode methods are only called from the worker entry point (public/frankenphp-worker.php). When running in traditional PHP-FPM mode via public/index.php, none of this code is executed. The changes introduce zero performance overhead for applications not using worker mode.

Benchmarks
I also prepared a set of simple benchmarks to evaluate worker mode performance: https://github.com/michalsn/benchmark-codeigniter-frankenphp

Based on existing benchmarks, nginx with PHP-FPM should perform similarly to FrankenPHP running in classic mode. When I attempted to verify this, the results showed roughly 60% of classic mode performance. However, these tests were run in a local development environment that is not properly tuned, which likely skewed the results.

For this reason, I decided not to include the benchmark numbers for nginx with PHP-FPM. In a real-world, properly configured environment, nginx is expected to perform similarly to FrankenPHP in classic mode.

Checklist:

  • Securely signed commits
  • Component(s) with PHPDoc blocks, only if necessary or adds value (without duplication)
  • Unit testing, with >80% coverage
  • User guide updated
  • Conforms to style guide

@michalsn michalsn added new feature PRs for new features 4.7 labels Jan 16, 2026
@github-actions github-actions bot added the stale Pull requests with conflicts label Jan 18, 2026
@codeigniter4 codeigniter4 deleted a comment from github-actions bot Jan 18, 2026
@michalsn michalsn removed the stale Pull requests with conflicts label Jan 18, 2026
Copy link
Member

@paulbalandan paulbalandan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to read up on this, but sharing initial comments.

@github-actions github-actions bot added the stale Pull requests with conflicts label Jan 19, 2026
@codeigniter4 codeigniter4 deleted a comment from github-actions bot Jan 19, 2026
@michalsn michalsn removed the stale Pull requests with conflicts label Jan 19, 2026
Copy link
Contributor

@neznaika0 neznaika0 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't fully appreciate PR - I've never tried frankenphp. I'll probably come back to this later.

After accepting the PR, you can contact the developers and add CI4 to the list of available frameworks in the Native support for section. It would be a bit of a boost and a reminder of CI4.

/**
* Checks if a service instance has been created.
*
* @param string $key Identifier of the entry to check.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these important comments? Or rename the $key > $serviceName would be clearer?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The $key is used throughout this class, including in other methods. The comments are applied for consistency with the other methods, so I think we're fine.

@neznaika0
Copy link
Contributor

I apply PR as patch for v4.7. phpstan have errors:

     [ErrorException]                                                                                                                                                    
     Class CodeIgniter\Cache\ReconnectCacheDecorator contains 1 abstract method and must therefore be declared abstract or implement the remaining method                
     (CodeIgniter\Cache\CacheInterface::remember)                                                                                                                        
     at SYSTEMPATH/Cache/ReconnectCacheDecorator.php:24 

@michalsn
Copy link
Member Author

I apply PR as patch for v4.7. phpstan have errors:

     [ErrorException]                                                                                                                                                    
     Class CodeIgniter\Cache\ReconnectCacheDecorator contains 1 abstract method and must therefore be declared abstract or implement the remaining method                
     (CodeIgniter\Cache\CacheInterface::remember)                                                                                                                        
     at SYSTEMPATH/Cache/ReconnectCacheDecorator.php:24 

I think something may have gone wrong when you applied the patch, because the final code no longer contains ReconnectCacheDecorator. It was there at one point, but I decided it would be too big of a BC break and rewrote the approach in one of the later iterations. I may squash the commits later to make it easier for everyone to follow.

michalsn and others added 4 commits January 22, 2026 20:51
Co-authored-by: John Paul E. Balandan, CPA <paulbalandan@gmail.com>
apply code suggestion

Co-authored-by: neznaika0 <ozornick.ks@gmail.com>
Copy link
Member

@paulbalandan paulbalandan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a great feature! I have read on the FrankenPHP's docs. Adding here some comments and I think this is good to go.

Co-authored-by: John Paul E. Balandan, CPA <paulbalandan@gmail.com>
@paulbalandan paulbalandan added breaking change Pull requests that may break existing functionalities and removed breaking change Pull requests that may break existing functionalities labels Jan 24, 2026
@neznaika0
Copy link
Contributor

Caddyfile uses tabs instead of spaces. I didn't find it in the documentation, the built-in formatter replaced them automatically.

@neznaika0
Copy link
Contributor

  File created: FCPATH/frankenphp-worker.php
  File created: ROOTPATH/Caddyfile

The paths in the terminal are clickable.
It's not very convenient. Can I specify the full path /home/www/...? It is unlikely that anyone will see the secret paths.

@neznaika0
Copy link
Contributor

2. Test your application:
   curl http://localhost:8080/

I understand that Caddyfile is configurable. But by default, the user can have a non-standard
app.baseUrl = https://hehe.fu:8181/
It is possible to apply data from .env App.php for the initial setup?

It's not necessary, just my opinion.

@neznaika0
Copy link
Contributor

I checked on the main page: speed 0.007 > 0.002 memory 0.951 > 2.890
I think it will be better on big data.

@michalsn
Copy link
Member Author

Caddyfile uses tabs instead of spaces. I didn't find it in the documentation, the built-in formatter replaced them automatically.

Caddy doesn't care whether you use tabs, spaces, or even no indentation at all.

  File created: FCPATH/frankenphp-worker.php
  File created: ROOTPATH/Caddyfile

The paths in the terminal are clickable. It's not very convenient. Can I specify the full path /home/www/...? It is unlikely that anyone will see the secret paths.

We use this approach pretty much everywhere in the CLI (including generators), so I'm not sure if I want to change it just for this case. Keeping paths relative also makes the output more consistent and portable across different environments.

2. Test your application:
   curl http://localhost:8080/

I understand that Caddyfile is configurable. But by default, the user can have a non-standard app.baseUrl = https://hehe.fu:8181/ It is possible to apply data from .env App.php for the initial setup?

It's not necessary, just my opinion.

By default, Caddy will listen on http://localhost:8080/. Using a different baseURL in your app config won't affect that - it will only result in incorrect URL generation in the app.

For the initial setup, we assume the default local Caddyfile is used. That's why the example uses http://localhost:8080/.

@ddevsr
Copy link
Collaborator

ddevsr commented Jan 29, 2026

Can we have two default Caddyfiles for Development and Production in the installer? I hope we can have ready-deploy to Docker too maybe with option --with-docker. I already have a sample in my project that I used for testing FrankenPHP long time ago, but stuck on bootstrap load cant finish yet.

Specifically, the watch configuration in the worker section for development will automatically reload the service, as described in the documentation here
.

Since Caddy supports HTTP/3, we can use a production-ready Caddyfile as the default for production environments.

Thanks for the hard work on this.

Caddyfile

{
    frankenphp {
        num_threads {$NUM_THREADS:4}

        worker {
            # Worker running
			file ${WORKDIR:/app}/public/index.php

            # Amount of Worker
            num {$NUM_WORKER:2} # Sets the number of PHP threads to start, defaults to 2x the number of available CPUs.

            # Watcher (you can reload automatically when file already changes files in project)
			watch
		}
	}
}

:{$HTTP_PORT:80}, :{$HTTPS_PORT:443} {
    # Enable compression (optional)
	encode gzip br zstd

    # Webroot
	root * ${WORKDIR:/app}/public

    # Route requests to index.php if not a file or directory
    @phpFiles {
        not file
        not path /favicon.ico
    }
    rewrite @phpFiles /index.php{uri}

    # Serve PHP files through FrankenPHP
    php_server {
        index index.php
    }

    # Must set value off for deploy production
	php_server off
}

Dockerfile

ARG PHP

FROM dunglas/frankenphp:php${PHP}

# Tentukan environment variables
ENV WORKDIR=${WORKDIR:-/app} \
    WORKER_MODE=${WORKER_MODE:-on} \
    SERVER_NAME=${SERVER_NAME:-localhost} \
    CI_ENVIRONMENT=${CI_ENVIRONMENT:-development} \
    NUM_THREADS=${NUM_THREADS:-4} \
    NUM_WORKER=${NUM_WORKER:-2}

COPY . ${WORKDIR}
WORKDIR ${WORKDIR}

RUN apt update && apt install libnss3-tools -y

# add additional extensions here:
RUN install-php-extensions \
	intl \
	bz2 \
	gd \
	gmagick \
	redis \
	zip \
	opcache

# Enable PHP production settings
RUN if [ "$CI_ENVIRONMENT" = "production" ]; then \
        mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"; \
    else \
        mv "$PHP_INI_DIR/php.ini-development" "$PHP_INI_DIR/php.ini"; \
    fi

# https://github.com/composer/docker/pull/250
COPY --from=composer/composer:2-bin /composer /usr/local/bin/composer

# Install vendor
RUN if [ "$CI_ENVIRONMENT" = "production" ]; then \
        composer install --no-dev --prefer-dist --no-interaction --no-scripts --optimize-autoloader; \
    else \
        composer install --prefer-dist --no-interaction --no-scripts --optimize-autoloader; \
    fi

# Remove Cache Manually
RUN rm -rf ${WORKDIR}/writable/cache/FactoriesCache*
RUN rm -rf ${WORKDIR}/writable/cache/FileLocatorCache*

RUN chmod -R 777 ${WORKDIR}/public/assets/uploads && chmod -R 777 ${WORKDIR}/writable

COPY --link Caddyfile /Caddyfile

CMD [ "frankenphp", "run", "--config", "/Caddyfile" ]

docker-compose.yml

services:
  php:
    container_name: myproject
    # uncomment the following line if you want to use a custom Dockerfile
    build:
      context: .
      dockerfile: Dockerfile
      args:
        PHP: ${PHP:-8.4}
    # uncomment the following line if you want to run this in a production environment
    restart: always
    environment:
      WORKDIR: ${WORKDIR:-/app}
      WORKER_MODE: ${WORKER_MODE:-on}
      SERVER_NAME: ${SERVER_NAME:-localhost}
      CI_ENVIRONMENT: ${CI_ENVIRONMENT:-development}
      NUM_THREADS: ${NUM_THREADS:-4}
      NUM_WORKER: ${NUM_WORKER:-2}
    ports:
      # HTTP
      - target: 80
        published: ${HTTP_PORT:-80}
        protocol: tcp
      # HTTPS
      - target: 443
        published: ${HTTPS_PORT:-443}
        protocol: tcp
      # HTTP/3
      - target: 443
        published: ${HTTP3_PORT:-443}
        protocol: udp
    volumes:
      - ./:${WORKDIR:-/app}
      - caddy_data:/data
      - caddy_config:/config
    depends_on:
      - redis
    # comment the following line in production, it allows to have nice human-readable logs in dev
    tty: true

  redis:
    image: redis:alpine
    container_name: redis-askred
    ports:
      - "6379:6379"

# Volumes needed for Caddy certificates and configuration
volumes:
  caddy_data:
  caddy_config:

@michalsn
Copy link
Member Author

@ddevsr Thanks for sharing the detailed setup and examples.

At the moment, we don't provide production-ready configs or Dockerfile setups for Nginx or Apache either, so I don't think it makes sense to treat FrankenPHP differently. Production environments and container setups can vary a lot, and shipping official "production" configs or Dockerfiles could be misleading.

I did consider adding a watch directive, but more in the context of a future hot-reload feature. That said, you may be right that enabling watch by default for development is desirable.

For now, I think we should stick with the current setup and potentially add more guidance and examples in the docs later.

Copy link
Collaborator

@ddevsr ddevsr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For me, this is pretty clear.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

4.7 new feature PRs for new features

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants