Running JekCMS on Shared Hosting: A Realistic Performance Guide from 14 Live Sites

All 14 of my jekcms sites run on shared hosting behind Cloudflare — the biggest has 2,300+ posts. What actually matters for speed, first-hand.

Running JekCMS on Shared Hosting: A Realistic Performance Guide from 14 Live Sites

All 14 of my jekcms sites run on shared hosting behind Cloudflare — the biggest has 2,300+ posts. What actually matters for speed, first-hand.

Everything in this post comes from my own hosting account. I run 14 content sites on jekcms — the CMS I build — and every one of them lives on shared hosting: LiteSpeed servers, Cloudflare in front. The biggest site has 2,300+ published posts; the fleet as a whole is past 5,500. There is no VPS hiding behind any of them. So when I say shared hosting is enough for a serious content site, I'm not repeating a benchmark I read somewhere. I'm describing the machines my traffic actually lands on.

It's also where I once got IP-banned by my own host in the middle of a deploy. Shared hosting is fine — but it punishes you for treating it like a VPS. That difference is what this guide is about.

Set expectations before you set config values

What you're buying on a shared plan: a slice of CPU that gets throttled the moment you're greedy, disk IO you share with strangers, a MySQL server you cannot tune, and no root. Usually LiteSpeed rather than Apache — which is good news, because it's fast and it reads your .htaccess anyway.

Two rules follow from that, and every section below is one of them in disguise:

  • Most requests should never reach PHP at all.
  • When PHP does run, it should do as little as possible.

How the cache layers stack on shared hosting

jekcms ships page, object and query caches — I wrote up the full stack separately — but on shared hosting one of them does almost all the work: the page cache. The first request for a URL renders the full HTML and writes it to disk. Every request after that, until the TTL runs out (300 seconds by default), is served from that file. No routing, no queries, no template rendering.

People ask why I don't raise the TTL to an hour. I could. But five minutes already means the expensive render almost never runs on any page people actually visit — and scheduled posts and edits show up promptly without me thinking about purges. I've never found a reason to touch it.

Above the page cache sits Cloudflare, which takes the static assets — images, CSS, JS — off my server entirely. Below it sits OPcache, which matters more than people assume, because even a page-cache hit is still PHP reading a file. And OPcache is where shared hosting bit me hardest.

Why I don't let Cloudflare cache the HTML itself

Tempting idea: put a "cache everything" rule on Cloudflare and let the edge serve full pages too. I tried it early on and walked it back. The moment the edge holds your HTML, every publish, every edit, every comment approval needs a purge call to Cloudflare as well — and when you forget one (you will), a stale page lives at the edge for hours with nothing on your server able to fix it. Static assets are the opposite case: their URLs change when their content changes, so they can sit at the edge for a year without risk. So my split is boring and deliberate: Cloudflare gets the assets, the origin page cache owns the HTML, and a five-minute TTL is my worst-case staleness. On shared hosting that division buys most of the win with none of the purge choreography.

The OPcache trap after every deploy

Here's the failure mode. You deploy new PHP files. They're on disk. But OPcache keeps serving compiled bytecode of the old files, and with opcache.revalidate_freq = 60 it won't even glance at the disk for up to a minute — longer if the host overrides validation settings, which some do. Meanwhile some files revalidate before others, and your site runs a mixture of old and new code. I have stared at a "fixed" bug happily reproducing in production because the fix existed on disk but not in OPcache.

My deploy pipeline now treats this as non-negotiable: the last step after rsync is an OPcache reset plus a page-cache purge. A deploy is not finished until both have run. If you take one habit from this post, take that one.

A .user.ini that reflects real usage

Most shared hosts let you override PHP settings with a .user.ini file in the site root. This is what mine have converged to:

; .user.ini (site root)
max_execution_time = 90
memory_limit = 256M
upload_max_filesize = 32M
post_max_size = 48M
max_input_vars = 5000
opcache.memory_consumption = 128
opcache.interned_strings_buffer = 16
opcache.max_accelerated_files = 10000
opcache.revalidate_freq = 60

The reasoning behind the less obvious ones:

  • max_execution_time = 90 — the slowest thing the admin panel ever does is converting a large photo to AVIF and WebP (jekcms resizes to a 1920px maximum and generates both formats on upload). The default 30 seconds survives one image; a batch upload on a throttled CPU sometimes doesn't.
  • memory_limit = 256M — image conversion again. Decoding a high-resolution photo into memory is the peak memory moment of the entire CMS.
  • max_input_vars = 5000 — large admin forms can post a lot of fields, and a low limit truncates the submission silently. Silent truncation is the worst kind of bug to chase.
  • opcache.memory_consumption = 128 — some panels default to 64, and a full OPcache quietly recompiles files in circles. That shows up as pages being intermittently slow with no pattern to it.

Quirk #1: your host may ban you for deploying

This is my favorite shared hosting story, because nothing in it was broken — and it still left half the fleet stranded.

My deploys run from GitHub Actions: one repo, rsync out to each of the 14 sites. Each rsync opened a fresh SSH connection, which meant a fresh authentication — fourteen of them, back to back, from the same IP, within seconds. To Hostinger's intrusion protection that looks exactly like a brute-force attempt, and it banned the runner's IP mid-loop. Half the sites got the new code. Half didn't. No error anywhere except "connection refused" from about site eight onward.

The fix isn't retries or sleep statements. It's SSH connection multiplexing:

# in the deploy job's ~/.ssh/config
Host deploy-target
  HostName your-server-host
  User your-user
  ControlMaster auto
  ControlPath ~/.ssh/cm-%r@%h:%p
  ControlPersist 5m

One authentication, one socket, and every subsequent rsync rides the same connection. The ban never triggered again. Two related rules I hold on shared hosting deploys: rsync runs without --delete, and .env, uploads/, cache/ and logs/ are excluded — server-side state is never the pipeline's to destroy. And for the day GitHub or SSH misbehaves, there's a fallback path: a server-side cron doing git pull can deploy the same repo from the inside.

Cron on shared hosting is a lottery — plan for losing it

Shared hosting crons range from "perfectly fine" to "minimum interval 30 minutes, silently disabled if the job misbehaves, settings buried three panel menus deep". Assume the worst tier, because scheduled publishing is exactly the feature you won't notice failing until a post quietly never goes live.

jekcms sidesteps this with a visitor-triggered scheduler. When no real cron is detected, the CMS checks a next-due-job timestamp and runs whatever is due — after the response has already been delivered to the visitor, so no request ever waits on it. It's deterministic, not probabilistic: a due job runs on the next request, full stop. If you've ever fought WP-Cron on a low-traffic site, you know why I'm emphasizing that word. And once you do configure a real cron, the fallback detects it and switches itself off — no double execution. The cron setup docs cover both modes and what the detection expects a real cron to look like.

My own setup: real cron where the panel allows a sane interval, the fallback everywhere else. I have genuinely stopped thinking about it.

What about the database?

A shorter section than you'd expect, because the page cache makes it one. You cannot tune MySQL on shared hosting — no buffer pool sizing, no config access — so the goal is to need less from it. With page caching on, the database sits mostly idle on the visitor path.

The one habit worth having: if your host exposes a slow query log, switch it on for a day after a big deploy or a content import. Anything consistently slow in there almost always points to a missing index, and ALTER TABLE ... ADD INDEX is one of the few server-side powers shared hosting actually leaves you.

Where shared hosting actually ends

Honesty section. The ceiling exists — it's just further away than the "shared hosting is a toy" crowd claims. Things that genuinely hurt on my plans: bulk imports of thousands of posts run noticeably slower than they would on dedicated CPU (they finish; they're just not fun), heavy admin work during a traffic spike competes with visitors for the same throttled resources, and you will never fix a noisy neighbor — some afternoons a site is simply slower, and the only honest explanation is that someone else's WordPress is being attacked.

What has not pushed me off shared hosting: 2,300+ posts on one site and 5,500+ across the fleet. Cached HTML doesn't care how many rows sit behind it. The day I need guaranteed CPU or my own MySQL config, I'll move — and the VPS configuration reference is written for exactly that day. It hasn't come yet, and that's the most honest performance benchmark I can offer.

Written by

Celil Uyanıkoğlu

Computer engineer with 25+ years in IT. He builds jekcms and runs his own network of content sites on it — every guide published here is tried on those live installs first.

See all posts →

Order Today

One-time payment, lifetime access. Setup in 30 minutes.

View Pricing
  • Setup and live in 30 minutes
  • 13 professional themes
  • AVIF/WebP image optimization
  • Automatic SEO — Sitemap, Schema.org
  • ZeroTrack cookieless analytics

Be the first to know

New features, release notes & CMS guides — a couple of emails a month, no spam.