David Lewin David Lewin
Cloud VPS & Hosting

Centralizing PHP, Nginx and WooCommerce error logs: Better Stack vs self-hosted Loki

Disclosure: some links in this guide are affiliate links. If you sign up through them, I may earn a commission at no extra cost to you. How this site makes money.

Short answer

Run one log shipper, Vector, on the store's server. It tails five files: the Nginx access log (written as JSON), the Nginx error log, the PHP error log, the PHP-FPM log and WooCommerce's wc-logs folder. It joins multi-line entries such as a fatal error's stack trace into one event, drops successful static-file requests, masks emails, card numbers and the last part of IP addresses, and sends the events to Better Stack, to a Loki server you run, or to both. A disk buffer and end-to-end acknowledgements kept every event through an outage and a crash in our test. Better Stack is the faster path: 3 GB a month free, then $0.10 to $0.35 per GB, with alerting and on-call built in. Loki costs a second server and your time, but the data and the alert rules stay with you.

When a WooCommerce checkout breaks, the reason is usually already written down, in one of five files on the server: Nginx's access and error logs, PHP's error log, PHP-FPM's log, or WooCommerce's own logs. Nobody reads them until a customer complains. This guide ships all five to one place, turns them into searchable events, and alerts on the ones that cost sales. It compares a managed service, Better Stack, with Grafana Loki on a server you run, using the same shipper config for both.

How this guide was made

We ran the setup below end to end. A real Nginx 1.30.6 served a WordPress 7.1.3 and WooCommerce 11.2.1 store through PHP 8.4. Test requests made the store write real entries: fatal errors on the checkout endpoint, warnings, 404s, 502 and 504 gateway errors, and WooCommerce log entries with an email address and a card number in them. Vector 0.59.0, using the config in this guide with only the file paths changed, shipped them to a real Loki 3.6.19 and to a mock of Better Stack's ingest API. The mock follows the documented contract: a bearer token, gzip, and a 202 response. Loki's ruler sent alerts to a mock Alertmanager. The PHP-FPM lines were written by the test, in the format PHP-FPM uses, because FPM doesn't run on Windows, where we ran the test. All 26 checks pass. We didn't use a Better Stack account; its pricing and limits are from its pricing page and docs (October 2026).

1. The short answer

QuestionAnswer
Which files?Nginx access (as JSON) and error logs, the PHP error log, the PHP-FPM log, wp-content/uploads/wc-logs/*.log
Which shipper?Vector: one agent that parses, filters and masks before anything leaves the server, and can send to both destinations
What gets dropped?Successful static-file requests (CSS, JS, images, fonts). Errors on static files are kept.
Personal dataEmails and card-like numbers become [REDACTED]; IP addresses are cut to their /24 network
OutagesDisk buffer plus acknowledgements: 50 of 50 events arrived after a 503 outage and a hard crash of the shipper
Managed optionBetter Stack: 3 GB of logs a month free (3-day retention), then $0.10 per GB in Europe plus $0.05 per GB per month to keep it
Self-hosted optionGrafana Loki on a second server, alert rules in LogQL, notifications through Alertmanager
What should page youPHP fatal errors, 5xx on checkout or cart, Nginx unable to reach PHP (502/504), PHP-FPM out of workers

2. The pipeline

Store logs: from the VPS to an alert
Store logs: from the VPS to an alertStore VPS → Vector → Disk buffer per destination → Better Stack: HTTPS, gzip, bearer token / Loki on a second VPS: private network or WireGuard only → Better Stack alerts: email, call, SMS, push / Loki ruler: LogQL rules, then Alertmanager to email, Slack or PagerDutyStore VPS: Nginx, PHP-FPM, WordPress and WooCommerce write 5 kinds of log fileStore VPSNginx, PHP-FPM, WordPress and WooCommerce write 5 kinds of logfileVector: tails the files, joins stack traces, parses to JSON, drops static 2xx, masks emails, cards and IPsVectortails the files, joins stack traces, parses to JSON, dropsstatic 2xx, masks emails, cards and IPsDisk buffer per destination: file positions advance only once the buffer holds the eventDisk buffer per destinationfile positions advance only once the buffer holds the eventBetter Stack: HTTPS, gzip, bearer tokenBetter Stack: HTTPS, gzip,bearer tokenLoki on a second VPS: private network or WireGuard onlyLoki on a second VPS:private network orWireGuard onlyBetter Stack alerts: email, call, SMS, pushBetter Stack alerts:email, call, SMS, pushLoki ruler: LogQL rules, then Alertmanager to email, Slack or PagerDutyLoki ruler: LogQL rules,then Alertmanager toemail, Slack or PagerDuty
One agent, one config. Keep the sink you want and delete the other, or run both while you compare.

Shipping logs is the easy part. What makes the difference is what happens on the server first. A PHP fatal error is ten lines in the file and should be one event. A product page pulls in dozens of CSS, JS and image requests that nobody will ever search. A WooCommerce log entry can contain a customer's email. Doing that work before shipping keeps the bill and the privacy risk small, whichever destination you pick.

3. Make the logs worth shipping

Nginx: write the access log as JSON

The default "combined" format has to be parsed back apart with a regex. JSON with escape=json arrives already split into fields, and a " in a user agent can't break it:

# /etc/nginx/conf.d/store-log.conf (inside http {})
log_format store_json escape=json '{"time":"$time_iso8601","ip":"$remote_addr","method":"$request_method",'
  '"uri":"$request_uri","status":$status,"bytes":$body_bytes_sent,"rt":$request_time,'
  '"urt":"$upstream_response_time","ua":"$http_user_agent","host":"$host"}';
access_log /var/log/nginx/access.json.log store_json;

rt is the full request time; urt is PHP's share of it. When urt climbs while rt - urt doesn't, the slowness is in PHP or the database, not the network.

PHP: one error log, in UTC

; /etc/php/8.3/fpm/conf.d/99-logging.ini
log_errors = On
error_log = /var/log/php/error.log
date.timezone = UTC
; keep display_errors = Off in production

PHP stamps each line with its zone ([21-Oct-2026 21:12:03 UTC]), and the config below reads that zone. Watch this: Vector reads a time without a zone as the server's local time. In our first run, PHP times landed five hours in the future because the test machine wasn't on UTC. Loki rejected them with 400 timestamp too new, and Vector dropped them, because a 400 isn't retried. Better Stack accepted the same events. So a wrong time doesn't always show up as a wrong time; with Loki it shows up as missing logs.

WooCommerce: move the logs out of the web root

WooCommerce writes one file per source per day: wc-logs/stripe-2026-10-21-<hash>.log. The lines look like 2026-10-21T21:00:53+00:00 CRITICAL Payment gateway timeout CONTEXT: {"order_id":1234}. A multi-line message continues on lines without a timestamp. WooCommerce guards the folder with an .htaccess file, which Nginx ignores. Since WooCommerce 8.8 you can move the folder with a filter:

<?php
// wp-content/mu-plugins/wc-log-dir.php
add_filter( 'woocommerce_log_directory', fn() => '/var/log/woocommerce/' );

If you leave it where it is, add location ^~ /wp-content/uploads/wc-logs/ { deny all; } to the Nginx site config.

4. The Vector config

Install Vector from its official packages, save this as /etc/vector/vector.yaml, put the endpoints and token in /etc/vector/secrets.json (mode 600, owned by the vector user), and add vector to the adm group or whichever group can read the logs. Since Vector 0.57, ${VAR} in config files is no longer expanded unless you pass a flag whose name starts with "dangerously"; the secrets file is the supported way.

# /etc/vector/vector.yaml
# Tails Nginx, PHP, PHP-FPM and WooCommerce logs, turns each entry into one structured event,
# drops noise, masks personal data, and ships to Better Stack and/or Loki (delete the sink you don't use).
data_dir: /var/lib/vector
# Vector 0.57+ no longer expands ${ENV} in config files: endpoints and tokens come from a root-only JSON file,
# {"betterstack_url": "https://<ingesting host>/", "betterstack_token": "...", "loki_url": "http://10.0.0.5:3100"}
secret:
  local:
    type: file
    path: /etc/vector/secrets.json

sources:
  nginx_access:
    type: file
    include: ["/var/log/nginx/access.json.log"]
  nginx_error:
    type: file
    include: ["/var/log/nginx/error.log"]
  php:
    type: file
    include: ["/var/log/php/error.log"]
    multiline:                      # a fatal error and its stack trace become one event
      start_pattern: '^\[\d{2}-[A-Za-z]{3}-\d{4} '
      condition_pattern: '^\[\d{2}-[A-Za-z]{3}-\d{4} '
      mode: halt_before
      timeout_ms: 1000
  php_fpm:
    type: file
    include: ["/var/log/php8.3-fpm.log"]
  woocommerce:
    type: file
    include: ["/var/www/shop/wp-content/uploads/wc-logs/*.log"]
    ignore_older_secs: 86400        # one file per source per day: don't replay last month on first start
    multiline:                      # multi-line messages continue on lines without a timestamp
      start_pattern: '^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}'
      condition_pattern: '^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}'
      mode: halt_before
      timeout_ms: 1000

transforms:
  nginx_access_parsed:
    type: remap
    inputs: [nginx_access]
    source: |
      .service = "nginx"
      j, err = parse_json(.message)
      if err == null && is_object(j) {
        . = merge!(., j)
        status = to_int(.status) ?? 0
        # Successful static files are most of the volume and never tell you anything: drop them here.
        if status < 400 && match(to_string(.uri) ?? "", r'\.(?:css|js|mjs|png|jpe?g|gif|webp|avif|svg|ico|woff2?)(?:\?|$)') {
          abort
        }
        .level = if status >= 500 { "error" } else if status >= 400 { "warning" } else { "info" }
        .timestamp = parse_timestamp(.time, "%+") ?? .timestamp
        del(.time)
        .message = join([to_string(.method) ?? "-", to_string(.uri) ?? "-", to_string(status)], " ")
      }

  nginx_error_parsed:
    type: remap
    inputs: [nginx_error]
    source: |
      .service = "nginx"
      p, err = parse_regex(.message, r'^(?P<ts>\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2}) \[(?P<lvl>\w+)\] \d+#\d+: (?:\*\d+ )?(?P<msg>.*)$')
      if err == null {
        .timestamp = parse_timestamp(p.ts, "%Y/%m/%d %H:%M:%S") ?? .timestamp   # local time, read in the same zone
        .level = p.lvl
        .message = strip_whitespace!(p.msg)
        r, err = parse_regex(p.msg, r'request: "(?P<request>[^"]*)"')
        if err == null { .request = r.request }
        u, err = parse_regex(p.msg, r'upstream: "(?P<upstream>[^"]*)"')
        if err == null { .upstream = u.upstream }
      }

  php_parsed:
    type: remap
    inputs: [php]
    source: |
      .service = "php"
      p, err = parse_regex(.message, r'(?s)^\[(?P<ts>\d{2}-[A-Za-z]{3}-\d{4} \d{2}:\d{2}:\d{2})(?: (?P<tz>[\w/+-]+))?\] (?:PHP (?P<kind>[A-Za-z ]+?):\s+)?(?P<msg>.*)$')
      if err == null {
        # PHP names its zone ("UTC"); without one, Vector would read the time in the server's local zone.
        .timestamp = parse_timestamp(p.ts, "%d-%b-%Y %H:%M:%S", timezone: string(p.tz) ?? "UTC") ?? .timestamp
        kind = downcase(p.kind) ?? ""
        .level = if contains(kind, "fatal") || contains(kind, "parse") { "critical" }
          else if kind == "warning" { "warning" }
          else if kind == "notice" || kind == "deprecated" { "notice" }
          else { "error" }                            # plain error_log() lines, e.g. "WordPress database error"
        .message = strip_whitespace!(p.msg)
      }

  php_fpm_parsed:
    type: remap
    inputs: [php_fpm]
    source: |
      .service = "php-fpm"
      p, err = parse_regex(.message, r'^\[(?P<ts>\d{2}-[A-Za-z]{3}-\d{4} \d{2}:\d{2}:\d{2})(?:\.\d+)?\] (?P<lvl>[A-Z]+): (?P<msg>.*)$')
      if err == null {
        .timestamp = parse_timestamp(p.ts, "%d-%b-%Y %H:%M:%S") ?? .timestamp
        .level = downcase!(p.lvl)
        .message = p.msg
      }

  woocommerce_parsed:
    type: remap
    inputs: [woocommerce]
    source: |
      .service = "woocommerce"
      f, err = parse_regex(.file, r'(?P<src>[^/\\]+?)-\d{4}-\d{2}-\d{2}-[0-9a-f]{32}\.log$')
      if err == null { .source = f.src }              # wc-logs/stripe-2026-10-21-<hash>.log -> "stripe"
      p, err = parse_regex(.message, r'(?s)^(?P<ts>\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[+-]\d{2}:\d{2}) (?P<lvl>[A-Z]+) (?P<msg>.*?)(?: CONTEXT: (?P<ctx>\{.*\}))?\s*$')
      if err == null {
        .timestamp = parse_timestamp(p.ts, "%+") ?? .timestamp
        .level = downcase!(p.lvl)
        .message = p.msg
        if p.ctx != null { .context = parse_json(p.ctx) ?? p.ctx }
      }

  ship:
    type: remap
    inputs: [nginx_access_parsed, nginx_error_parsed, php_parsed, php_fpm_parsed, woocommerce_parsed]
    source: |
      .host = get_hostname() ?? "unknown"
      lvl = to_string(.level) ?? "info"
      .level = string(get({"warn": "warning", "crit": "critical", "emerg": "emergency", "err": "error"}, [lvl]) ?? null) ?? lvl
      # Personal data never leaves the server: emails and card-like digit runs, anywhere in the event.
      file = del(.file)
      . = redact(., filters: [r'[\w.+-]+@[\w-]+(?:\.[\w-]+)+', r'\b(?:\d[ -]?){12,18}\d\b'])
      .file = file
      # Keep the network, not the person: 203.0.113.42 -> 203.0.113.0
      if is_ipv4(.ip) ?? false { .ip = ip_subnet(.ip, "/24") ?? null } else if is_string(.ip) { .ip = ip_subnet(.ip, "/48") ?? null }
      msg = replace(to_string(.message) ?? "", r'client: (\d+\.\d+\.\d+)\.\d+', "client: ${1}.0")
      .message = truncate(msg, 8192)
      del(.source_type)

  to_better_stack:
    type: remap
    inputs: [ship]
    source: .dt = del(.timestamp)

sinks:
  better_stack:
    type: http
    inputs: [to_better_stack]
    uri: "SECRET[local.betterstack_url]"
    method: post
    encoding: { codec: json }
    compression: gzip
    auth: { strategy: bearer, token: "SECRET[local.betterstack_token]" }
    buffer: { type: disk, max_size: 268435488, when_full: block }   # survives outages and restarts
    acknowledgements: { enabled: true }   # file checkpoints move only once the buffer has the event

  loki:
    type: loki
    inputs: [ship]
    endpoint: "SECRET[local.loki_url]"   # over a private network or a WireGuard tunnel
    encoding: { codec: json }
    labels:                           # few, bounded labels; everything else stays in the JSON line
      service: "{{ service }}"
      level: "{{ level }}"
      host: "{{ host }}"
    out_of_order_action: accept
    buffer: { type: disk, max_size: 268435488, when_full: block }
    acknowledgements: { enabled: true }

The details that matter:

  • Multi-line: a new PHP entry starts with [dd-Mon-yyyy; anything else, such as Stack trace: and #0 ..., belongs to the entry before it. In the test, each fatal error arrived as one event with its stack trace.
  • Static files: 30 asset requests in the test were dropped at the server. A 404 on /missing.css was kept, as a warning.
  • Masking: runs over the whole event, so it catches personal data in messages, URLs and WooCommerce's context JSON. A password-reset URL with ?email= in it arrived as [REDACTED]. The card pattern looks for 13 to 19 digits with optional spaces or dashes. Check that your order numbers are shorter.
  • ignore_older_secs: without it, the first start ships every WooCommerce log file still on disk.
  • Labels: Loki indexes labels, not content. Three labels with a handful of values each keep it fast; a label per URL or status code would create thousands of streams.

5. Destination A: Better Stack

Create a source in Better Stack and choose Vector or HTTP. It gives you an ingesting host and a source token for the secrets file. The documented contract: POST / with Authorization: Bearer, JSON, NDJSON or MessagePack, gzip allowed, up to 10 MiB per request, the event time in a field called dt, and 202 on success. A full free quota returns 402 and a wrong token 403. In our test Vector retried the 503s and dropped the 400s that Loki sent, so treat any 4xx, the 402 for a full quota included, as lost events: watch Vector's log for "Non-retriable error".

Alerts are queries: you write a Live tail query or SQL against the source, then pick a threshold, a relative-change or an anomaly alert, how often it runs, and who gets it (email by default; call, SMS, push or an escalation policy). The useful starting set mirrors the Loki rules below: any service = php event at level critical, three or more status >= 500 on /checkout, wc-ajax= or /wc/store/ within five minutes, and any "upstream timed out" or "connect() failed" line.

6. Destination B: Loki you run

Loki runs as one binary on a small second server. Put it on the same private network as the store, or behind WireGuard. With auth_enabled: false anyone who can reach port 3100 can read and write every log.

# /etc/loki/config.yaml (single binary, filesystem storage, 31 days)
auth_enabled: false
server: {http_listen_address: 10.0.0.5, http_listen_port: 3100, grpc_listen_port: 9096}
common:
  instance_addr: 127.0.0.1
  path_prefix: /var/lib/loki
  storage: {filesystem: {chunks_directory: /var/lib/loki/chunks, rules_directory: /var/lib/loki/rules}}
  replication_factor: 1
  ring: {kvstore: {store: inmemory}}
schema_config:
  configs: [{from: 2026-01-01, store: tsdb, object_store: filesystem, schema: v13, index: {prefix: index_, period: 24h}}]
limits_config:
  retention_period: 744h
  discover_service_name: []      # Loki 3 otherwise adds a service_name label of its own
compactor: {working_directory: /var/lib/loki/compactor, retention_enabled: true, delete_request_store: filesystem}
ruler:
  alertmanager_url: http://127.0.0.1:9093
  storage: {type: local, local: {directory: /etc/loki/rules}}
  rule_path: /var/lib/loki/ruler-tmp
  enable_api: true

The alert rules, in LogQL:

# Loki ruler: /etc/loki/rules/fake/store-alerts.yaml ("fake" is the tenant when auth_enabled is false)
groups:
  - name: store-errors
    interval: 1m
    rules:
      - alert: PHPFatalError
        expr: sum by (host) (count_over_time({service="php", level="critical"}[5m])) > 0
        labels: { severity: page }
        annotations: { summary: "PHP fatal error on {{ $labels.host }}" }

      - alert: CheckoutServerErrors
        expr: |
          sum by (host) (count_over_time({service="nginx", level="error"} | json
            | uri =~ `.*(checkout|wc-ajax=|/wc/store/).*` [5m])) >= 3
        labels: { severity: page }
        annotations: { summary: "{{ $value }} server errors on checkout or cart requests in 5 minutes" }

      - alert: PHPUpstreamFailing
        expr: sum by (host) (count_over_time({service="nginx"} |~ `upstream timed out|connect\(\) .*failed` [5m])) > 0
        labels: { severity: page }
        annotations: { summary: "Nginx can't get answers from PHP on {{ $labels.host }} (502/504)" }

      - alert: WooCommerceCritical
        expr: sum by (host) (count_over_time({service="woocommerce", level=~"critical|alert|emergency"}[10m])) > 0
        labels: { severity: ticket }
        annotations: { summary: "WooCommerce logged a critical entry" }

      - alert: PHPFPMOutOfWorkers
        expr: sum by (host) (count_over_time({service="php-fpm"} |= "max_children" [15m])) > 0
        labels: { severity: ticket }
        annotations: { summary: "PHP-FPM hit pm.max_children: requests are queueing" }

Point Grafana at Loki to search, and run Alertmanager next to it to route the alerts. That's three services to update and back up, which is the real cost of this route.

7. Test results

CheckResult
Each PHP fatal error with its stack traceOne event, level critical
Same fatal also in WooCommerce's fatal-errors logYes: one error, two events. The rules alert on the PHP one and treat WooCommerce's as a ticket
3-line WooCommerce messageOne event, context JSON parsed (order_id kept as a number)
504 (PHP too slow) and 502 (PHP down)In the access log as errors, and in the error log with request and upstream as fields
Email and card number in a WooCommerce entryBoth [REDACTED], in the message and in the context
IP addressesCut to the /24, including the client: inside Nginx error lines
Better Stack returns 503, Vector killed, restarted, service back50 of 50 requests delivered, 0 duplicates
Loki during the same outage50 of 50, unaffected: each destination has its own buffer
Log rotation (rename, then nginx -s reopen)20 of 20 lines from before and after
Loki labelsOnly service, level, host
All 5 Loki alert rulesFired and reached Alertmanager
Size on the wireAbout 350 bytes of JSON per access-log event; gzip brought requests down to 15% to 39% of that, depending on batch size

One difference showed up between the two destinations. Three identical fatal errors within one second were three events at Better Stack and two in Loki. Loki keeps one copy of entries with the same timestamp and the same line in a stream, and PHP's log only has whole seconds. That doesn't change a "more than zero" alert, but don't count identical errors in Loki and expect an exact number.

8. What it costs

An estimate, not a measurement: a store with 20,000 page views a day, and about four logged requests per page view once static files are dropped (the page, cart fragments, AJAX and Store API calls). At about 350 bytes per event, that's around 28 MB a day, or 0.85 GB a month. Bots can multiply it by three to ten.

Better StackSelf-hosted Loki
Logs at about 1 GB a monthInside the free 3 GB, kept for 3 days. Longer retention: $0.10 per GB ingested plus $0.05 per GB per month stored (Europe; US $0.15 and $0.08)A second server with 2 to 4 GB of RAM, plus its disk and backups
On-callResponder: $34 a month, or $29 billed yearly, which covers Uptime and Telemetry (pricing page, October 2026). We didn't check which alert channels need itAlertmanager is free; phone calls need a paging service
Your timeSetup in an afternoonSetup plus updates for Loki, Grafana and Alertmanager, and keeping the box secure
Data locationBetter Stack's regions (Europe, US, Singapore are priced separately)Wherever your server is

At store scale, the per-GB price is not the deciding factor; at 1 GB a month either way costs little. What decides is whether someone on the team wants to run three more services, and whether the logs may leave your infrastructure. Better Stack also does uptime checks and status pages, so a store that wants those anyway gets them in the same account.

Related: checkout monitoring with UptimeRobot vs scripts, StatusCake vs DIY SSL and API monitoring and carding attacks on WooCommerce, which this log setup will make visible.

9. Tools

Top pick

ToolRoleWhy it fits
Better StackHosted logs, uptime and on-callTakes Vector's output as is: a bearer token and an ingesting host. 3 GB of logs a month free with 3-day retention; then $0.10 per GB ingested and $0.05 per GB per month retained in Europe. Alerts on SQL or Live tail queries, with threshold, relative and anomaly types, and call, SMS and push through its on-call product (pricing page and docs, October 2026).

Alternatives: free tools and references

We don't earn anything from these links.

  • Grafana Loki: the self-hosted destination in this guide, with Grafana to search and Alertmanager to route alerts.
  • Elastic Stack (ELK): full-text search over everything, at the price of more memory and more to run. Vector has an Elasticsearch sink, so the config above only changes at the end.
  • Vector: the shipper. Grafana's own agent, Alloy, does the same job if you go all-in on Grafana.
  • Vector 0.57 upgrade guide: why ${ENV} stopped working in config files.

10. Pitfalls

  • Times without a zone: read in the server's local zone. Run servers on UTC and keep PHP's date.timezone set.
  • Trusting a 400 to be retried: it isn't. Loki rejects too-new and too-old entries, and the shipper drops them.
  • A memory buffer: the default. It loses whatever was queued when the shipper restarts. Use a disk buffer with acknowledgements.
  • High-cardinality labels: URL, user agent or order ID as a Loki label. Keep them in the line and filter with | json.
  • Personal data in logs: query strings and WooCommerce context carry emails. Mask on the server, before it's someone else's data too.
  • Exposing Loki: no auth by default. Private network only.
  • wc-logs in the web root under Nginx: the .htaccess guard does nothing. Move the folder or deny it.
  • Alerting on every warning: page on fatals, checkout 5xx and PHP being unreachable; send the rest to a daily review.
Bottom line

Do the work on the server: one Vector config that joins stack traces, drops static noise, masks personal data and buffers to disk. After that the destination is a business choice. Better Stack gets a small team searching and alerting in an afternoon, at a few dollars a month or nothing at store volumes. Loki keeps the data on your servers at the cost of running three more services.

David Lewin
David Lewin

Founder of StealthScale.io and e-commerce infrastructure engineer writing field-tested playbooks on proxies, cloud VPS, anti-detect browsers and SEO.