> ## Documentation Index
> Fetch the complete documentation index at: https://blog.onetsolutions.net/llms.txt
> Use this file to discover all available pages before exploring further.

# Black Friday: 7 server checks before the traffic spike

> Caching, load testing, snapshots, real restores, TTFB alerts, DNS TTL and security: seven checks to run before the sale, not during it.

*Published on October 18, 2026*

Black Friday falls on Friday, November 27 this year. A traffic spike rarely breaks a healthy server. It exposes what was already fragile: a cache that is installed but serves nothing, a backup nobody has ever restored, a plugin that auto-updated itself at 3 a.m. on the day.

The seven checks below belong to the weeks before the sale, not the day before. Each one gives the why, the how, and what a good result looks like. They apply to WordPress, WooCommerce, PrestaShop or a small application, on a VPS or on shared hosting.

| # | Check | Applies to | Good result |
| - | - | - | - |
| 1 | Page and object cache | VPS and shared | Second request served from cache, cart never cached |
| 2 | Load test | Mostly VPS | Thresholds hold at your target traffic, no swap, no 5xx |
| 3 | Snapshot and update freeze | VPS and shared | A dated rollback point, no plugin auto-updates during the sale |
| 4 | Restored backups | VPS and shared | One real restore completed, and its duration known |
| 5 | TTFB monitoring | VPS and shared | An alert on slowness, not only on downtime |
| 6 | DNS TTL | Any migration | TTL at 300 s at least 48 h before the switch |
| 7 | Security under load | Mostly VPS | Rules in place and tested before the sale |

## 1. Caching: check that it serves, not that it is installed

A traffic spike is mostly anonymous visitors looking at the same pages. If every one of those views runs PHP and queries the database, the server does the same work a thousand times over. Three layers matter:

* **the page cache**: the full HTML page, served to logged-out visitors without touching PHP;
* **the object cache**: SQL query results held in memory, typically in Redis, which speeds up what the page cache cannot serve;
* **the exclusions**: what must **never** be cached.

Start with the exclusions. The [WooCommerce documentation](https://developer.woocommerce.com/docs/best-practices/performance/configuring-caching-plugins) is explicit: the Cart, My Account and Checkout pages stay out of the cache, as do requests carrying the `woocommerce_cart_hash`, `woocommerce_items_in_cart` and `wp_woocommerce_session_` cookies. A cached cart is one customer's basket shown to another.

Then confirm the page cache actually answers, as an anonymous visitor:

```bash theme={null}
# Three requests in a row on a public page: the second should be faster
for i in 1 2 3; do
  curl -s -o /dev/null -w "HTTP %{http_code}  TTFB %{time_starttransfer}s\n" https://example.com/
done

# Cache headers returned by the server
curl -s -o /dev/null -D - https://example.com/ | grep -iE "^x-.*cache|^cache-control|^age:"
```

Our web hosting plans [include LiteSpeed](/en/boost-your-site-speed-by-400-percent). With the LiteSpeed Cache plugin on WordPress, the [LiteSpeed documentation](https://docs.litespeedtech.com/lscache/lscwp/installation/) names the header to look for: `X-LiteSpeed-Cache: hit` from the second request onwards, and `X-LiteSpeed-Cache-Control: no-cache` on pages that are deliberately excluded.

```bash theme={null}
# The cart must answer no-cache
curl -s -o /dev/null -D - https://example.com/cart/ | grep -i "x-litespeed-cache"
```

### Redis as an object cache, on a VPS

```bash theme={null}
sudo apt install redis-server

# Redis must listen locally only
ss -ltnp | grep 6379
redis-cli ping
```

A cache with no ceiling ends up eating the RAM that PHP and the database need on the day of the spike. Set a limit and an eviction policy in `/etc/redis/redis.conf`:

```ini theme={null}
# Example value: size it to your VPS memory
maxmemory 256mb
# Recommended by the Redis documentation as the default choice for a cache
maxmemory-policy allkeys-lru
```

```bash theme={null}
sudo systemctl restart redis-server
```

On the WordPress side, the [Redis Object Cache](https://wordpress.org/plugins/redis-cache/) plugin is driven from WP-CLI:

```bash theme={null}
wp plugin install redis-cache --activate
wp config set WP_REDIS_HOST "127.0.0.1"
wp config set WP_REDIS_PORT "6379"
# A prefix matters if several sites share the same Redis
wp config set WP_REDIS_PREFIX "shop_"
wp redis enable
wp redis status
```

After a few hours of normal traffic, look at the hit rate:

```bash theme={null}
redis-cli INFO stats | grep -E "keyspace_(hits|misses)|evicted_keys"
```

The [Redis documentation](https://redis.io/docs/latest/develop/reference/eviction/) gives the formula: `keyspace_hits / (keyspace_hits + keyspace_misses)`. A low rate alongside many `evicted_keys` means the ceiling is too low.

<Warning>
  A Redis reachable from the internet without a password is a way in, not a cache. See our [RediShell](/en/redishell-cve-2025-49844) advisory for the `bind` and ACL settings.
</Warning>

**PrestaShop.** Under Advanced Parameters > Performance, keep the Smarty cache enabled, do not use forced template compilation (it is meant for theme editing), and make sure debug mode is off. The [PrestaShop 8 documentation](https://docs.prestashop-project.org/v.8-documentation/user-guide/configuring-shop/advanced-parameters/performance) advises testing the store after enabling the CCC JavaScript cache.

**Good result:** a `hit` cache header on public pages, `no-cache` on cart and checkout, and a TTFB clearly lower on the second request than on the first.

## 2. A load test, before the day

You do not know how many visitors your server can take until you have measured it. The only good time to find out is before the sale.

[k6](https://grafana.com/docs/k6/latest/) is open source, runs in one command through Docker, and exits with an error code if your thresholds are not met. Run it from **a different machine** than the server under test, or you will be measuring your laptop more than your site.

```javascript theme={null}
// load.js
import http from 'k6/http';
import { check, sleep } from 'k6';

const BASE = __ENV.BASE_URL;

export const options = {
  // Example stages: start from last year's peak, taken from your analytics
  stages: [
    { duration: '2m', target: 20 },
    { duration: '5m', target: 20 },
    { duration: '2m', target: 50 },
    { duration: '5m', target: 50 },
    { duration: '1m', target: 0 },
  ],
  thresholds: {
    // Fewer than 1% errors
    http_req_failed: ['rate<0.01'],
    // 95% of responses with a first byte under 800 ms
    http_req_waiting: ['p(95)<800'],
  },
};

export default function () {
  // A cached page, a product page, a search (never cached)
  const pages = ['/', '/product/example/', '/?s=gift&post_type=product'];
  for (const path of pages) {
    const res = http.get(`${BASE}${path}`);
    check(res, { 'status 200': (r) => r.status === 200 });
    sleep(1);
  }
}
```

```bash theme={null}
docker run --rm -i grafana/k6 run -e BASE_URL=https://example.com - <load.js
```

According to the [k6 metrics reference](https://grafana.com/docs/k6/latest/using-k6/metrics/reference/), `http_req_waiting` is the time spent waiting for the first byte: the TTFB. While the test runs, watch the server alongside it:

```bash theme={null}
# si/so columns: swap; wa: disk wait. One line every 5 seconds
vmstat 5

# Web server errors during the test window
sudo tail -f /var/log/nginx/error.log
```

<Warning>
  Only test your own site, and never the payment flow: you would create real orders and trip your payment provider's fraud filters. On shared hosting, the server is shared with other customers: talk to [support](https://help.onetsolutions.net/console/support) before running anything.
</Warning>

If the thresholds break, fix the cache first: it is almost always cheaper than a bigger server. If the server saturates even with a cache that serves, resize the instance from the console, and do it before the week of the sale. A resize is a change like any other: it goes through the snapshot in the next section.

**Good result:** k6 exits with code 0 at your target traffic, with no swap and no 5xx in the logs.

## 3. A snapshot before every change, then the freeze

Every change before the sale (cache configuration, resizing, the last updates) starts with a [snapshot](https://help.onetsolutions.net/vps/snapshot) from the console. It takes a minute and gives you a dated rollback point.

For a VPS snapshot to capture a database in a consistent state, the [QEMU guest agent](https://help.onetsolutions.net/vps/agent) must be running: it freezes the filesystem during the capture.

```bash theme={null}
systemctl is-active qemu-guest-agent
```

<Note>
  A snapshot is not a backup. It gets you back from a failed change, not through the loss of the server. That is what section 4 is for.
</Note>

Then **freeze**. Apply every update before the sale and none during it: no deployments, no plugin or theme updates, no configuration changes. Keep WordPress core minor auto-updates on, though, since they carry the security fixes, as explained in [our WordPress hardening guide](/en/secure-wordpress-after-wp2shell).

```bash theme={null}
# Record which plugins auto-update, so you can restore them after the sale
wp plugin auto-updates status --all --enabled-only --field=name > ~/auto-update-plugins.txt

# Pause plugin and theme auto-updates
wp plugin auto-updates disable --all
wp theme auto-updates disable --all

# After the sale: restore the original settings
xargs -r wp plugin auto-updates enable < ~/auto-update-plugins.txt
```

Write down the one exception now: a critical, actively exploited flaw during the freeze. [wp2shell](/en/wp2shell-cve-2026-63030) was exploited within hours of disclosure. In that case: snapshot, patch, check the checkout flow. Nothing else.

**Good result:** a snapshot taken after the last updates, the QEMU agent active, and an empty list from `wp plugin auto-updates status --all --enabled-only` during the sale.

## 4. Backups you have actually restored

A backup that has never been restored is an assumption. The 3-2-1 rule, as worded in the [US-CERT paper published by CISA](https://www.cisa.gov/sites/default/files/publications/data_backup_options.pdf):

* **3** copies of any important file: the original and two backups;
* on **2** different media types;
* with **1** stored offsite.

Applied to a site hosted with us:

| Copy | VPS | Web hosting |
| - | - | - |
| 1. Production | The server | The cPanel account |
| 2. Backup at your host | The [Backup](https://help.onetsolutions.net/vps/backup) option, daily | A full [cPanel backup](https://help.onetsolutions.net/web-hosting/backup) |
| 3. Offsite | An export to another provider or your own premises | The cPanel archive, downloaded and kept elsewhere |

The snapshot does not count: it lives in the same place as the server.

Then test restoring the database, the part that breaks most often:

```bash theme={null}
# Consistent dump of an InnoDB database, without locking tables
mysqldump --single-transaction --quick --routines --triggers shop | gzip > ~/shop-$(date +%F).sql.gz

# Restore into a throwaway database, timed
mysql -e "CREATE DATABASE restore_test"
time (gunzip -c ~/shop-$(date +%F).sql.gz | mysql restore_test)

# Compare order counts: wp_wc_orders if the store uses HPOS,
# otherwise wp_posts with post_type = 'shop_order'
mysql -N -e "SELECT COUNT(*) FROM shop.wp_wc_orders"
mysql -N -e "SELECT COUNT(*) FROM restore_test.wp_wc_orders"

# Clean up
mysql -e "DROP DATABASE restore_test"
```

Adjust the database name and table prefix to your installation. On PrestaShop, compare `ps_orders`.

**Good result:** the counts match, the site comes up on the test environment, and you know how long a full restore takes. That number tells you how many sales you would lose on a Friday evening.

## 5. Monitor TTFB, not just uptime

A typical uptime probe is satisfied with a 200. A store that answers 200 in nine seconds is up for the probe, and closed for your customers.

Measure TTFB (time to first byte). [web.dev](https://web.dev/articles/ttfb) rates a TTFB of 0.8 s or less as good and anything above 1.8 s as poor, noting that this is a rough guide rather than a Core Web Vital. Start there, then adjust to your own measurements.

Watch two URLs: a cached page, which measures the web server, and the cart, which is never cached and so measures PHP and the database. The cart is what slows down first.

```bash theme={null}
#!/usr/bin/env bash
# ttfb-check.sh — run it from a different machine than the monitored server
set -u
URLS=("https://example.com/" "https://example.com/cart/")
THRESHOLD=0.8                                 # seconds
WEBHOOK="https://hooks.example.com/your-webhook"  # Slack or Mattermost incoming webhook
STATE=/var/tmp/ttfb-check
mkdir -p "$STATE"

for url in "${URLS[@]}"; do
  key=$(printf '%s' "$url" | md5sum | cut -c1-12)
  result=$(curl -o /dev/null -s --max-time 10 -w '%{http_code} %{time_starttransfer}' "$url")
  code=${result%% *}
  ttfb=${result##* }
  if [ "$code" != "200" ] || awk -v t="$ttfb" -v s="$THRESHOLD" 'BEGIN { exit !(t > s) }'; then
    n=$(( $(cat "$STATE/$key" 2>/dev/null || echo 0) + 1 ))
    echo "$n" > "$STATE/$key"
    # Alert on the third consecutive failure, not on a single blip
    if [ "$n" -eq 3 ]; then
      curl -s -X POST -H 'Content-Type: application/json' \
        -d "{\"text\": \"$url: HTTP $code, TTFB ${ttfb}s\"}" "$WEBHOOK" > /dev/null
    fi
  else
    echo 0 > "$STATE/$key"
  fi
done
```

```bash theme={null}
sudo install -m 755 ttfb-check.sh /usr/local/bin/ttfb-check.sh
# One measurement a minute: alert after three minutes of slowness
( crontab -l 2>/dev/null; echo "* * * * * /usr/local/bin/ttfb-check.sh" ) | crontab -
```

If you prefer graphs, Uptime Kuma is part of the [One Click Apps](/en/one-click-apps) catalogue and charts the response time of every probe. Install it on a different server than the store: monitoring that goes down with the site warns nobody.

**Good result:** a few days of measurements before the sale so you know your normal level, and a test alert received on the right channel, by the right person.

## 6. DNS: lower the TTL 48 h before any migration

The best DNS advice for Black Friday week: do not migrate. If you must move servers anyway, prepare the switch in advance.

Resolvers keep a record cached for its full TTL. If your `A` record has a one-day TTL, some of your visitors will keep landing on the old server for up to a day after the change. So lower the TTL **first**, then wait at least the old duration before switching. Forty-eight hours cover the usual one-day TTLs with room to spare.

Read the configured TTL straight from an authoritative server, not from your resolver's cache:

```bash theme={null}
NS=$(dig +short NS example.com | head -1)
dig +noall +answer example.com A @"$NS"
dig +noall +answer www.example.com A @"$NS"
# The second field of each line is the TTL, in seconds
```

If the old TTL is longer than 48 h, wait correspondingly longer. In your domain's DNS zone in the console, the TTL field offers 5 minutes, which is 300 seconds. If the domain is linked to a web hosting plan, the zone is managed in cPanel. The page on [pointing a domain at a VPS](https://help.onetsolutions.net/vps/redirect-my-domain) covers the records to write.

After the switch, keep the old server online as long as its access log still receives requests, then raise the TTL again.

**Good result:** `dig` against the authoritative server shows `300` at least 48 h before the switch.

## 7. Security under load: decided before, not during

Friday evening is not the time to find out how to ban an IP address. Everything below gets set up and tested now.

**Plugins up to date before the freeze.**

```bash theme={null}
wp core version
wp plugin list --update=available --fields=name,version,update_version
```

**fail2ban on SSH.** Since Debian 12, `rsyslog` is no longer installed by default according to the [release notes](https://www.debian.org/releases/bookworm/amd64/release-notes/ch-information.en.html): there is no `/var/log/auth.log`, so the `sshd` jail has to read the systemd journal. The Debian 13 and Ubuntu 24.04 packages already do this; the Debian 12 one does not. Setting it in `jail.local` covers all three.

```ini theme={null}
# /etc/fail2ban/jail.local
[DEFAULT]
# Your fixed addresses (office, agency) must never be banned
ignoreip = 127.0.0.1/8 ::1 203.0.113.10

[sshd]
enabled = true
backend = systemd
```

**Rate-limit where it helps, and nowhere else.** On WordPress, the login page is the usual target. The [nginx hardening guide](/en/audit-and-harden-nginx-on-a-vps) covers `limit_req` in detail. For `wp-login.php`:

```nginx theme={null}
# In the http block — example rate, adjust it
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;
```

```nginx theme={null}
# In the server block, before the generic PHP location
location = /wp-login.php {
    limit_req zone=login burst=5 nodelay;
    include snippets/fastcgi-php.conf;
    # Match the socket to your PHP version
    fastcgi_pass unix:/run/php/php8.4-fpm.sock;
}
```

The `fastcgi_pass` is not optional: without it, nginx would serve the PHP file as a static download. fail2ban then ships an `nginx-limit-req` filter, which reads the nginx error log and bans addresses that repeatedly exceed the limit:

```ini theme={null}
# Add to /etc/fail2ban/jail.local
[nginx-limit-req]
enabled = true
backend = auto
logpath = /var/log/nginx/error.log
```

The `backend = auto` line is not decoration: Ubuntu 24.04 points every jail at the systemd journal by default, while nginx writes its errors to a file.

```bash theme={null}
sudo nginx -t && sudo systemctl reload nginx
sudo systemctl restart fail2ban
sudo fail2ban-client status sshd
sudo fail2ban-client status nginx-limit-req
```

Put no limit on your payment provider's callback URLs, nor on `/wp-json/` as a whole: a blocked payment webhook is a paid order stuck in "pending".

**WooCommerce: throttle checkout attempts.** If your store uses the Checkout block, WooCommerce offers checkout rate limiting under WooCommerce > Settings > Advanced > Features. According to the [Store API documentation](https://developer.woocommerce.com/docs/apis/store-api/rate-limiting/), it is off by default and, once enabled, caps checkout at 3 requests per 60 seconds. Place a test order after turning it on.

**The procedure, in writing.** Who is on call, and how to ban an address by hand:

```bash theme={null}
sudo fail2ban-client set nginx-limit-req banip 198.51.100.7
sudo fail2ban-client set nginx-limit-req unbanip 198.51.100.7
```

<Warning>
  Do not switch on the [console firewall](https://help.onetsolutions.net/vps/firewall) in a hurry. As soon as the first rule is added, it blocks all traffic that is not explicitly allowed. If you want one, set it up well before the sale, with the SSH, HTTP and HTTPS rules. If something goes wrong, the [KVM console](https://help.onetsolutions.net/vps/kvm-console) remains available.
</Warning>

**Good result:** no plugin waiting for an update, both fail2ban jails active, a test order placed with WooCommerce rate limiting on, and a procedure someone other than you can follow.

## What this actually buys you

None of this makes a server unbreakable. These checks move the work: away from Friday evening, when every minute costs sales, and into the weeks before, when a mistake costs nothing more than a snapshot rollback.

If you only have time for two, run the load test and the restore. They are the two that expose what you assume is already handled.

Questions about a server hosted with us? [Our team is available](https://help.onetsolutions.net/console/support) from your client area.

<Info>
  **Black Friday at OnetSolutions.** From Friday, November 27, 2026 at 00:00 to Tuesday, December 1, 2026 at 23:59 (Paris time), a single offer on the first year of our VPS and web hosting plans on annual billing. New services only, once per customer. Renewal is at the normal annual price. [How it works and the dates](https://onetsolutions.net/en/black-friday).
</Info>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.