Cookbook

Performance Recipes

Short performance recipes — cache-then-invalidate, HTTP caching for public endpoints, and reverse-proxy caching. The full reference is Advanced → Performance.

A few end-to-end recipes. The reference for Glueful's performance surface (query caching, #[CacheResult], db:profile, the query analyzer and optimizer, response caching, configuration tuning) is Advanced → Performance.

Cache a Read, Invalidate on Write

Cache data in a service (not Response objects), and drop the entry in the same service that writes:

use Glueful\Cache\CacheStore;
use Glueful\Database\Connection;

final class CatalogService
{
    public function __construct(
        private Connection $db,
        private CacheStore $cache,
    ) {}

    public function featured(): array
    {
        return $this->cache->remember('catalog:featured', function () {
            return $this->db->table('products')
                ->where('featured', true)
                ->orderBy(['popularity' => 'DESC'])
                ->limit(20)
                ->get();
        }, 600);
    }

    public function setFeatured(string $uuid, bool $featured): void
    {
        $this->db->table('products')->where('uuid', $uuid)->update(['featured' => $featured]);

        $this->cache->delete('catalog:featured');
    }
}

If you use per-query caching (->cache()) instead, invalidate the table's cached queries after the write:

app($context, \Glueful\Database\QueryCacheService::class)->invalidateTable('products');

Table invalidation uses cache tags, which the redis and file stores support (file since 1.91; before, tag invalidation silently did nothing on the default store). On memcached, which has no tags, delete keys you know instead.

HTTP Caching for a Public Endpoint

For data that's the same for every client, let browsers and CDNs cache it, and answer revalidation with 304:

use Glueful\Http\Response;

// routes/api.php
$router->get('/config/public', [PublicConfigController::class, 'show'])
    ->middleware('conditional_cache:etag');

// Controller
public function show(): Response
{
    $config = $this->configService->publicConfig();

    return Response::success($config)
        ->withCacheHeaders(3600, true)               // Cache-Control: public, max-age=3600
        ->withETag(md5(json_encode($config)));
}

For per-user data, use withCacheHeaders(300, false) (private) or no caching headers at all.

Reverse-Proxy Caching

Put nginx (or Varnish, or a CDN) in front of cacheable GET endpoints:

proxy_cache_path /var/cache/nginx/api levels=1:2 keys_zone=api_cache:10m max_size=100m;

location /api/v1/config/public {
    proxy_pass http://backend;
    proxy_cache api_cache;
    proxy_cache_valid 200 1h;
    proxy_cache_key "$request_uri";
    add_header X-Cache-Status $upstream_cache_status;
}

Only cache responses that don't depend on Authorization or cookies. Actively purging a CDN needs the glueful/cdn extension; without it, edge purges are no-ops.

Find the Slow Query

# What does the database do with it?
php glueful db:profile "SELECT * FROM orders WHERE customer_uuid = ?" --params='["c-123"]' --explain --patterns

# Does the rewrite help?
php glueful db:profile --file=current.sql --compare=candidate.sql

Then index what the plan scans, and re-run. See Query Analysis and Profiling.