Operations
- Manage authentication
- Manage large volume log streams with automatic stream sharding
- Manage varying workloads at scale with autoscaling queriers
- Block unwanted queries
- Manage bloom filter building and querying (Deprecated)
- Configure caches to speed up queries
- Kafka-based ingestion (Experimental)
- Use label-based access control with Loki
- Manage tenant isolation
- Monitor tenant limits using the Overrides Exporter
- Manage recording rules
- Enforce rate limits and push request validation
- Manage larger production deployments
- Manage version upgrades
- Speed up ingester rollout using zone awareness
- Deploy Loki meta-monitoring with chart version 3
- Deploy Loki meta-monitoring
- Key metrics for monitoring Loki
- Install dashboards, alerts, and recording rules
- Single binary meta-monitoring
- Horizontal scaling of Compactor
- Filesystem object store
- Log entry deletion
- Log retention
- Single Store TSDB (tsdb)
- Write Ahead Log
- Troubleshoot Logs Drilldown
- Troubleshoot log ingestion (WRITE)
- Troubleshoot Loki operations
- Troubleshoot log queries (READ)
Manage authentication
Grafana Loki does not come with any included authentication layer. You must run an authenticating reverse proxy in front of your services.
The simple scalable and microservices deployment modes require a reverse proxy to be deployed in front of Loki, to direct client API requests to the various components.
By default the Loki Helm chart includes a default reverse proxy configuration, using an nginx container to handle routing traffic and authorization.
A list of open-source reverse proxies you can use:
- HAProxy
- nginx using their guide on restricting access with HTTP basic authentication
- OAuth2 proxy
- Pomerium, which has a guide for securing Grafana
{{< admonition type="note" >}}
Loki runs in multi-tenant mode by default (auth_enabled: true). In this mode, Loki requires the HTTP header
X-Scope-OrgID to be set to a string identifying the tenant on every request; the responsibility
of populating this value should be handled by the authenticating reverse proxy.
A request that does not include this header fails with an HTTP 401 Unauthorized error and the message no org id.
To learn how to troubleshoot this error, refer to No org ID.
For more information, read the multi-tenancy documentation.{{< /admonition >}}
For information on configuring authentication for your log shipping agent, see the Grafana Alloy documentation.
Mutual TLS
Loki's HTTP and gRPC servers can require clients to present a valid TLS certificate before a connection is accepted. This is a form of client authentication that Loki supports natively, without a reverse proxy.
Mutual TLS (mTLS) authenticates the client at the transport level. It does not populate X-Scope-OrgID, so you still need a reverse proxy, your log shipping agent, or your client to set that header when auth_enabled: true.
To require client certificates, you must first enable TLS on the server by setting a certificate and a key, then set a client authentication policy and the certificate authority (CA) that signs your client certificates. For example:
server:
http_tls_config:
cert_file: /etc/loki/certs/server.crt
key_file: /etc/loki/certs/server.key
# Reject any client that does not present a certificate signed by this CA.
client_auth_type: RequireAndVerifyClientCert
client_ca_file: /etc/loki/certs/ca.crt
Use client_ca_file for the path to a CA certificate file, or client_ca to include the certificate contents directly in the configuration file. The grpc_tls_config block accepts the same options for the gRPC server. The equivalent command line flags are -server.http-tls-client-auth and -server.http-tls-ca-path, plus -server.grpc-tls-client-auth and -server.grpc-tls-ca-path.
The supported values for client_auth_type are:
NoClientCertRequestClientCertRequireAnyClientCertVerifyClientCertIfGivenRequireAndVerifyClientCert
Use RequireAndVerifyClientCert for the strictest setting. It rejects a client that sends no certificate, and it also rejects a certificate that the CA did not sign. The other values check less. For example, VerifyClientCertIfGiven allows a client that sends no certificate at all. For the full list of TLS options, refer to the server block of the configuration reference.
Enable basic authentication for Loki using nginx
This section describes the process of enabling basic authentication for Loki using nginx.
Prerequisites
- A running Loki instance
- A running nginx instance
Configure nginx
You must create a new nginx configuration file for the Loki instance.
This example assumes the following:
- nginx is running in
/opt/homebrew - Loki is running on port 3100 on the local machine
- Your Loki tenant id is
fake - The configuration file is named
/opt/homebrew/etc/nginx/loki.conf
If you used different configuration parameters for Loki, adjust the examples to match your configuration.
loki.conf configuration:
upstream loki {
server 127.0.0.1:3100;
keepalive 15;
}
server {
listen 80;
server_name loki.localhost;
auth_basic "loki auth";
auth_basic_user_file /opt/homebrew/etc/nginx/passwords;
location / {
proxy_read_timeout 1800s;
proxy_connect_timeout 1600s;
proxy_pass http://loki;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Keep-Alive";
proxy_set_header Proxy-Connection "Keep-Alive";
proxy_redirect off;
}
location /ready {
proxy_pass http://loki;
proxy_http_version 1.1;
proxy_set_header Connection "Keep-Alive";
proxy_set_header Proxy-Connection "Keep-Alive";
proxy_redirect off;
auth_basic "off";
}
}
{{< admonition type="tip" >}}
This example expects the client to send the X-Scope-OrgID header itself, in addition to the basic authentication credentials. If you run Loki with auth_enabled: true, you can instead have nginx set the tenant ID from the authenticated username. Add this line to the location / {} block:
proxy_set_header X-Scope-OrgID $remote_user;
With this line, nginx overwrites any tenant header sent by the client, so a client cannot read or write the data of another tenant. This requires each username in your password file to match a Loki tenant ID. The Loki Helm chart's gateway uses this same pattern.{{< /admonition >}}
This configuration must be included in your main nginx configuration, for example, by including it in nginx.conf like:
include /opt/homebrew/etc/nginx/loki.conf;
Restart the nginx server to ensure all configuration changes are updated.
Validate your nginx configuration
To validate the nginx configuration for Loki, you can send a curl request to two endpoints:
- The
/readyendpoint, which is not protected by a basic authentication mechanism.
% curl -i http://loki.localhost/ready
HTTP/1.1 200 OK
Server: nginx/1.29.2
Date: Thu, 16 Oct 2025 14:28:31 GMT
Content-Type: text/plain; charset=utf-8
Content-Length: 6
Connection: keep-alive
X-Content-Type-Options: nosniff
ready
- The
/endpoint, which is protected by a basic authentication mechanism.
curl -i http://loki.localhost/
HTTP/1.1 401 Unauthorized
Server: nginx/1.29.2
Date: Thu, 16 Oct 2025 14:32:43 GMT
Content-Type: text/html
Content-Length: 179
Connection: keep-alive
WWW-Authenticate: Basic realm="loki auth"
<html>
<head><title>401 Authorization Required</title></head>
<body>
<center><h1>401 Authorization Required</h1></center>
<hr><center>nginx/1.29.2</center>
</body>
</html>
Update passwords
The password file can be seeded using whatever mechanism you may use for other web services.
In this example, htpasswd is utilized:
% htpasswd -c /opt/homebrew/etc/nginx/passwords loki123
New password:
Re-type new password:
Adding password for user loki123
Restart the nginx server to ensure all configuration changes are updated.
Validate passwords
Enter your password into a temporary file, such as:
% vi lokipw
Then, store it as an environment variable::
% pass=$(cat lokipw)
You can validate basic authentication is then working by issuing a curl command to the protected resource:
curl -i -u loki123:$pass -H "X-Scope-OrgID:fake" "http://loki.localhost/loki/api/v1/labels"
HTTP/1.1 200 OK
Server: nginx/1.29.2
Date: Thu, 16 Oct 2025 14:46:09 GMT
Content-Type: application/json; charset=UTF-8
Content-Length: 21
Connection: keep-alive
{"status":"success"}
Manage large volume log streams with automatic stream sharding
Automatic stream sharding can keep streams under a desired_rate by adding new labels and values to
existing streams. When properly tuned, this can eliminate issues where log producers are rate limited due to the
per-stream rate limit.
Automatic stream sharding is enabled by default (shard_streams.enabled defaults to true). To tune it or confirm it's on:
-
Check or set the global
limits_configin the Loki configuration file:limits_config: shard_streams: enabled: true -
Optionally lower the
desired_ratein bytes if you find that the system is still hitting theper_stream_rate_limit:limits_config: shard_streams: enabled: true desired_rate: 2097152 #2MiBThe
desired_ratedefaults to1536KB(1.5MB), which is below the defaultper_stream_rate_limitof3MB. This gives Loki some headroom to shard a stream before it hits the per-stream rate limit. -
Optionally enable
time_sharding_enabledif you need to ingest old, out-of-order logs, for example during a backfill:limits_config: shard_streams: enabled: true time_sharding_enabled: true time_sharding_ignore_recent: 40mTime-based sharding adds a
__time_shard__label to streams, splitting log entries into buckets based on their timestamp. Log entries with a timestamp newer thantime_sharding_ignore_recent(40 minutes by default) are still ingested, but Loki does not add the__time_shard__label to them. This lets very old logs be ingested without triggering out-of-order errors. Refer to How automatic stream sharding works for more detail. -
Optionally enable
logging_enabledfor debugging stream sharding. {{< admonition type="note" >}} This may affect the ingestion performance of Loki. {{< /admonition >}}limits_config: shard_streams: enabled: true logging_enabled: true
When to use automatic stream sharding
Large log streams present several problems for Loki, namely increased and uneven resource usage on Ingesters and Distributors. The general recommendation is to explore existing log streams for additional label values that are both useful for querying and sufficiently low cardinality. There are many cases, however, where no more labels can be extracted, or cardinality for a label is dangerously large. To protect itself from such volume leading to operational failure, Loki implements per-stream rate limits; but the result is that some data is lost. The per-stream limit also needs human intervention to change, which is not ideal when log volumes increase and decrease.
Loki uses automatic stream sharding to avoid rate limiting and large streams for any log stream by ensuring it is close
to a configured desired_rate.
How automatic stream sharding works
Loki supports two independent sharding mechanisms, rate-based sharding and time-based sharding. You can enable either one on its own, or enable both together.
Rate-based sharding
Rate-based sharding works by adding a new label, __stream_shard__, to streams and incrementing its value to try
and keep all streams below a configured desired_rate.
The feature adds a new API to Ingesters that reports the size of all existing log streams. Once per second, Distributors
query the API to get a picture of all stream rates in the system. Distributors use the existing stream-rate data and a
configured desired_rate to determine how many shards a given stream should have. The desired number of new log streams
are created with the label __stream_shard__ and logs are divided evenly among the streams.
The once-per-second query interval is a Distributor setting, not a shard_streams limit. Use the -distributor.rate-store.stream-rate-update-interval flag, or the equivalent rate_store block in the Distributor configuration, to change how often Distributors refresh their view of stream rates.
Because rate-based sharding is reactive and relies on successive calls to Ingesters, the view of current rates is
always somewhat behind. As a result, the actual size of sharded streams will always be higher than the desired_rate.
In practice, this is still sufficient to keep log producers from being rate limited by per-stream rate limits.
Time-based sharding
Time-based sharding works by adding a different label, __time_shard__, to streams. Loki calculates the value of this label from each log entry's timestamp, divided into buckets of max_chunk_age/2. This lets Loki accept out-of-order log entries that are older than would normally be allowed, because each time bucket becomes its own stream instead of extending an existing one.
Loki does not apply time-based sharding to log entries with a timestamp newer than the configured time_sharding_ignore_recent value. Those entries are still ingested, but they keep their original labels, so that recent logs are not needlessly split into extra streams.
If you enable both rate-based sharding and time-based sharding, Loki applies time-based sharding first, and then applies rate-based sharding to each of the resulting time-sharded streams.
Automatic stream sharding metrics
Use these metrics to help tune Loki so that it is sharding streams aggressively enough to avoid the per-stream rate limit:
loki_rate_store_refresh_failures_total: The total number of failed attempts to refresh the distributor's view of stream rates.loki_rate_store_streams: The number of unique streams reported by all Ingesters. Sharded streams are reported as if they were unsharded.loki_rate_store_max_stream_shards: The maximum number of shards for any tenant of the system.loki_rate_store_stream_shards: A histogram of the distribution of shard counts across all streams.loki_rate_store_max_stream_rate_bytes: The maximum stream size in bytes/second for any tenant of the system. Sharded streams are reported as if they are unsharded.loki_rate_store_max_unique_stream_rate_bytes: The maximum size of any stream across all tenants. Stream shards are individually reported.loki_rate_store_stream_rate_bytes: A histogram of the distribution of stream sizes across all tenants in bytes/second.loki_stream_sharding_count: The total number of times that streams have been sharded. Useful for calculating the sharding rate.
Manage varying workloads at scale with autoscaling queriers
A microservices deployment of a Loki cluster that runs on Kubernetes typically handles a workload that varies throughout the day. To make Loki easier to operate and optimize the cost of running Loki at scale, we have designed a set of resources to help you autoscale your Loki queriers.
Prerequisites
You need to run Loki in Kubernetes as a set of microservices. You need to use the query-scheduler.
We recommend using Kubernetes Event-Driven Autoscaling (KEDA) to configure autoscaling based on Prometheus metrics. Refer to Deploying KEDA to learn more about setting up KEDA in your Kubernetes cluster.
Scaling metric
Because queriers pull queries from the query-scheduler queue and process them on the querier workers, you should scale metrics based on:
- The scheduler queue size.
- The queries running in the queriers.
The query-scheduler exposes the loki_query_scheduler_inflight_requests metric.
It tracks the sum of queued queries plus the number of queries currently running in the querier workers.
The following query is useful to scale queriers based on the inflight requests.
sum(
max_over_time(
loki_query_scheduler_inflight_requests{namespace="loki-cluster", quantile="<Q>"}[<R>]
)
)
Use the quantile (Q) and the range (R) parameters to fine-tune the metric. The higher Q is, the more sensitive the metric is to short-lasting spikes. As R increases, you can reduce the variation over time in the metric. A higher R-value helps avoid the autoscaler from modifying the number of replicas too frequently.
In our experience, we have found that a Q of 0.75 and an R of 2 minutes work well. You can adjust these values according to your workload.
loki_query_scheduler_inflight_requests only tracks a fixed set of quantiles: 0.5, 0.75, 0.8, 0.9, 0.95, and 0.99.
If you query the metric with any other quantile value, Prometheus returns no data.
Choose Q from this list.
The metric already smooths its value over a 60 second sliding window before Prometheus scrapes it. Because of this, an R value below about one minute adds little extra smoothing. We recommend keeping R at one minute or higher.
Cluster capacity planning
To scale the Loki queries, you configure the following settings:
- The threshold for scaling up and down
- The scale down stabilization period
- The minimum and the maximum number of queriers
Querier workers process queries from the queue. You can configure each Loki querier to run several workers with the
-querier.max-concurrent flag (max_concurrent in YAML configuration), which defaults to 4.
To reserve workforce headroom to address workload spikes, our recommendation is not to use more than 75% of the workers.
For example, if you configure the Loki queriers to run 6 workers, set a threshold of floor(0.75 * 6) = 4.
-querier.max-concurrent sets the total number of workers for a querier, but the querier splits that total across its connections to query-schedulers (or query-frontends).
If the configured value is lower than the number of query-schedulers a querier connects to, each connection still gets at least one worker, so the real worker count can be higher than the configured value.
Check the actual number with the loki_querier_worker_concurrency metric before you calculate the threshold.
To determine the minimum number of queries that you should run, run at least one querier and determine the average number of inflight requests the system processes 75% of the time over seven days. The target utilization of the queries is 75%. So if we use 6 workers per querier, we will use the following query:
clamp_min(ceil(
avg(
avg_over_time(loki_query_scheduler_inflight_requests{namespace="loki-cluster", quantile="0.75"}[7d])
) / scalar(floor(vector(6 * 0.75)))
), 1)
The maximum number of queriers to run is equal to the number of queriers required to process all inflight requests 50% of the time during a seven-day timespan. As for the previous example, if each querier runs 6 workers, divide the inflight requests by 6. The resulting query becomes:
ceil(
max(
max_over_time(loki_query_scheduler_inflight_requests{namespace="loki-cluster", quantile="0.5"}[7d])
) / 6
)
To minimize the scenario where Loki scales up shortly after scaling down, set a stabilization window for scaling down.
KEDA configuration
If you deploy Loki with the Helm chart, you don't need to write your own KEDA ScaledObject.
The chart can generate one for you through the querier.kedaAutoscaling values, which already default to a Prometheus trigger on loki_query_scheduler_inflight_requests with a Q of 0.75 and an R of 2 minutes.
Set querier.kedaAutoscaling.enabled to true, then set minReplicas and maxReplicas for your workload.
You also need to set defaults.kedaAutoscaling.prometheusAddress to the address of your Prometheus server, because it is empty by default and the trigger cannot query the metric without it.
To replace the default trigger, set querier.kedaAutoscaling.triggers.
Refer to Helm chart values for the full list of values.
If you don't use the Helm chart, or need a trigger the chart doesn't support, follow the example below to write your own KEDA ScaledObject.
This KEDA ScaledObject example configures autoscaling
for the querier deployment in the loki-cluster namespace.
The example shows the minimum number of replicas set to 10 and the maximum number of replicas set to 50.
Because each querier runs 6 workers, aiming to use 75% of those workers, the threshold is set to 4.
The metric is served at http://prometheus.default:9090/prometheus. We configure a stabilization window of 30 minutes.
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: querier
namespace: loki-cluster
spec:
maxReplicaCount: 50
minReplicaCount: 10
scaleTargetRef:
kind: Deployment
name: querier
triggers:
- metadata:
metricName: querier_autoscaling_metric
query: sum(max_over_time(loki_query_scheduler_inflight_requests{namespace="loki-cluster", quantile="0.75"}[2m]))
serverAddress: http://prometheus.default:9090/prometheus
threshold: "4"
type: prometheus
advanced:
horizontalPodAutoscalerConfig:
behavior:
scaleDown:
stabilizationWindowSeconds: 1800
Prometheus alerting when at capacity
Because the configured maximum might not be sufficient, a Prometheus alert can identify
when the quantity of queriers has been at its configured maximum for an extended time. The following example specifies three hours (3h) as the extended time:
name: LokiAutoscalerMaxedOut
expr: kube_horizontalpodautoscaler_status_current_replicas{namespace=~"loki-cluster"} == kube_horizontalpodautoscaler_spec_max_replicas{namespace=~"loki-cluster"}
for: 3h
labels:
severity: warning
annotations:
description: HPA {{ $labels.namespace }}/{{ $labels.horizontalpodautoscaler }} has been running at max replicas for longer than 3h; this can indicate underprovisioning.
summary: HPA has been running at max replicas for an extended time
Block unwanted queries
In certain situations, you may not be able to control the queries being sent to your Loki installation. These queries may be intentionally or unintentionally expensive to run, and they may affect the overall stability or cost of running your service.
You can block queries using per-tenant overrides, like so:
overrides:
"tenant-id":
blocked_queries:
# block this query exactly
- pattern: 'sum(rate({env="prod"}[1m]))'
# block any query matching this regex pattern
- pattern: '.*prod.*'
regex: true
# block all metric queries
- types: metric
# block any filter or limited queries matching this regex pattern
- pattern: '.*prod.*'
regex: true
types: filter,limited
# block any query that matches this query hash
- hash: 2943214005 # hash of {stream="stdout",pod="loki-canary-9w49x"}
types: filter,limited
# block queries originating from specific sources via X-Query-Tags
# Keys and values are matched case-insensitively.
- pattern: '.*' # optional; if pattern and regex are omitted they will default to '.*' and true
regex: true
query_tags:
source: grafana
feature: beta
{{< admonition type="note" >}} Changes to these configurations do not require a restart; they are defined in the runtime configuration file. {{< /admonition >}}
The available query types are:
metric: a query with an aggregation, e.g.sum(rate({env="prod"}[1m]))filter: a query with a log filter, e.g.{env="prod"} |= "error"limited: a query without a filter or a metric aggregation
The hash option uses a 32-bit FNV-1 hash of the query string, represented as a 32-bit unsigned integer.
This can often be easier to use than query strings that are long or require lots of string escaping. A query_hash field
is logged with every query request in the query-frontend and querier logs, for easy reference. Here's an example log line:
level=info ts=2023-03-30T09:08:15.2614555Z caller=metrics.go:152 component=frontend org_id=29 latency=fast
query="{stream=\"stdout\",pod=\"loki-canary-9w49x\"}" query_hash=2943214005 query_type=limited range_type=range ...
{{< admonition type="note" >}}
The order of patterns is preserved, and Loki stops at the first rule whose pattern or hash matches the query text.
If that rule's types or query_tags constraints then don't match, the query is not blocked, and Loki does not continue checking any later rules, even if a later rule would have matched and blocked the query.
For example, with this configuration:
overrides:
"tenant-id":
blocked_queries:
- pattern: '{env="prod"}'
types: metric
- pattern: '{env="prod"}'
A query for {env="prod"} (a limited query) matches the first rule's pattern, but not its types: metric constraint, so it is not blocked. Loki does not fall through to the second rule, which has no types restriction and would otherwise have blocked it. To block a query with more than one rule that share a pattern, combine the constraints into a single rule instead of relying on a later rule as a fallback.
{{< /admonition >}}
Observing blocked queries
Blocked queries are logged, as well as counted in the loki_blocked_queries metric on a per-tenant basis.
When a policy matches by pattern/hash/regex, Loki logs whether the query type and request tags matched that policy:
level=warn msg="query blocker matched with regex policy" user=29 type=metric pattern=".*rate\\(.*\\).*" query="sum(rate({app=\"foo\"}[5m]))" typesMatched=true tagsMatched=false blocked=false
If tag constraints fail to match, Loki emits a debug log showing the missing key and the raw header value that was received:
level=debug msg="query blocker tags mismatch: missing or mismatched key" key=feature tagsRaw="Source=grafana,Feature=alpha"
Scope
Query blocking is enforced by the LogQL query engine when it evaluates a query. This covers:
- Range and instant queries sent to the
/loki/api/v1/queryand/loki/api/v1/query_rangeAPI endpoints. - Alerting and recording rules, in both local and remote rule evaluation modes. Remote evaluation sends the rule's query back through the query frontend and querier, so it is blocked the same way as an API query.
Query blocking does not apply to:
- Log tailing (the
/loki/api/v1/tailendpoint). Tailing reads from the ingesters and the store without using the query engine, so no block policy is applied. - Metadata endpoints, such as
labels,series,index/stats,index/volume, anddetected_fields. These endpoints don't run a LogQL expression through the query engine, so there is nothing for the query blocker to match against.
Tag-based blocking
You can scope a blocked query rule to requests that include specific key=value pairs in the X-Query-Tags header.
- Header format:
key=valuepairs separated by commas, for example:Source=grafana,Feature=beta. - Allowed characters are alphanumeric plus space, comma, equals, '@', '.', and '-'. Any other characters are replaced with
_. - Parsing keeps only canonical
key=valuetokens; malformed tokens are ignored. - Matching rules:
- Keys are matched case-insensitively (the server lowercases keys).
- Values are matched case-insensitively.
- All specified
tags:pairs in the rule must be present in the request to apply the block.
Examples:
overrides:
tenant-a:
blocked_queries:
# Block only metric queries from a beta feature flag
- types: metric
query_tags:
feature: beta
# Combine with regex to narrow scope further
- pattern: '.*rate\\(.*\\).*'
regex: true
query_tags:
source: grafana
Ruler query tags
When the ruler evaluates alerting and recording rules in remote evaluation mode (-ruler.evaluation.mode=remote), it automatically sets X-Query-Tags on the query it sends to source=ruler,rule_name=<rule name>,rule_type=<rule type>.
You can match on these tags to block or scope a policy to rule evaluations specifically, for example:
overrides:
"tenant-id":
blocked_queries:
# block all queries that come from rule evaluation
- query_tags:
source: ruler
{{< admonition type="note" >}}
The ruler doesn't set X-Query-Tags in local evaluation mode, so you can't match on these tags there.
Be careful when you match on rule_name, because a rule name that contains a comma or an equals sign doesn't survive tag parsing. A comma ends the value early, so rule_name=my,rule is read as rule_name=my. An equals sign makes the pair invalid, so rule_name=a=b is dropped completely. Refer to Tag-based blocking for the full parsing rules.
{{< /admonition >}}
Manage bloom filter building and querying (Deprecated)
{{< admonition type="warning" >}} In Loki and Grafana Enterprise Logs (GEL), Query acceleration using blooms is deprecated and will be removed in a future release. Engineering and on-call support is not available. No SLA is provided. Note that this feature is intended for users who are ingesting more than 75TB of logs a month, as it is designed to accelerate queries against large volumes of logs. {{< /admonition >}}
Loki leverages bloom filters to speed up queries by reducing the amount of data Loki needs to load from the store and iterate through. Loki is often used to run "needle in a haystack" queries; these are queries where a large number of log lines are searched, but only a few log lines match the query. Some common use cases are searching all logs tied to a specific trace ID or customer ID.
An example of such queries would be looking for a trace ID on a whole cluster for the past 24 hours:
{cluster="prod"} | traceID="3c0e3dcd33e7"
Without accelerated filtering, Loki downloads all the chunks for all the streams matching {cluster="prod"} for the last 24 hours and iterates through each log line in the chunks, checking if the structured metadata key traceID with value 3c0e3dcd33e7 is present.
With accelerated filtering, Loki is able to skip most of the chunks and only process the ones where we have a statistical confidence that the structured metadata pair might be present.
To learn how to write queries to use bloom filters, refer to Query acceleration.
Enable bloom filters
{{< admonition type="warning" >}} Building and querying bloom filters are by design not supported in single binary deployment, it can only be used with distributed microservice mode. The reason is that bloom filters also come with a relatively high cost for both building and querying the bloom filters that only pays off at large scale deployments. {{< /admonition >}}
To start building and using blooms you need to:
- Deploy the Bloom Planner and Builder components (as microservices or via the SSD
backendtarget) and enable the components in the Bloom Build config. - Deploy the Bloom Gateway component (as a microservice or via the SSD
backendtarget) and enable the component in the Bloom Gateway config. - Enable blooms building and filtering for each tenant individually, or for all of them by default.
# Configuration block for the bloom creation.
bloom_build:
enabled: true
planner:
planning_interval: 8h
builder:
planner_address: bloom-planner.<namespace>.svc.cluster.local:9095
# Configuration block for bloom filtering.
bloom_gateway:
enabled: true
client:
addresses: dnssrvnoa+_bloom-gateway-grpc._tcp.bloom-gateway-headless.<namespace>.svc.cluster.local
# Enable blooms creation and filtering for all tenants by default
# or do it on a per-tenant basis.
limits_config:
bloom_creation_enabled: true
bloom_split_series_keyspace_by: 1024
bloom_gateway_enable_filtering: true
For more configuration options refer to the Bloom Gateway, Bloom Build and per tenant-limits configuration docs. We strongly recommend reading the whole documentation for this deprecated feature before using it.
Bloom Planner and Builder
Building bloom filters from the chunks in the object storage is done by two components: the Bloom Planner and the Bloom Builder, where the planner creates tasks for bloom building, and sends the tasks to the builders to process and upload the resulting blocks. Bloom filters are grouped in bloom blocks spanning multiple streams (also known as series) and chunks from a given day. To learn more about how blocks and metadata files are organized, refer to the Building blooms section below.
The Bloom Planner runs as a single instance and calculates the gaps in fingerprint ranges for a certain time period for a tenant for which bloom filters need to be built. It dispatches these tasks to the available builders. The planner also applies the blooms retention.
{{< admonition type="warning" >}} Do not run more than one instance of the Bloom Planner. {{< /admonition >}}
The Bloom Builder is a stateless horizontally scalable component and can be scaled independently of the planner to fulfill the processing demand of the created tasks.
You can find all the configuration options for these components in the Configure section for the Bloom Builder. Refer to the Enable bloom filters section above for a configuration snippet enabling this feature.
Retention
The Bloom Planner applies bloom block retention on object storage. Retention is disabled by default.
When enabled, retention is applied to all tenants. The retention for each tenant is the longest of its configured general retention (retention_period) and the streams retention (retention_stream).
For example, in the following example, tenant A has a bloom retention of 30 days, and tenant B a bloom retention of 40 days for the {namespace="prod"} stream.
overrides:
"A":
retention_period: 30d
"B":
retention_period: 30d
retention_stream:
- selector: '{namespace="prod"}'
priority: 1
period: 40d
Planner and Builder sizing and configuration
The single planner instance runs the planning phase for bloom blocks for each tenant in the given interval and puts the created tasks to an internal task queue.
Builders process tasks sequentially by pulling them from the queue. The amount of builder replicas required to complete all pending tasks before the next planning iteration depends on the value of -bloom-build.split-keyspace-by, the number of tenants, and the log volume of the streams.
The maximum block size is configured per tenant via -bloom-build.max-block-size.
The actual block size might exceed this limit given that we append streams blooms to the block until the block is larger than the configured maximum size.
The maximum size of a single stream's bloom is configured per tenant via -bloom-build.max-bloom-size. A stream whose bloom exceeds this size is discarded rather than added to a block.
Blocks are created in memory and as soon as they are written to the object store they are freed. Chunks and TSDB files are downloaded from the object store to the file system.
We estimate that builders are able to process 4MB worth of data per second per core.
Planning strategies
The Bloom Planner supports two strategies for splitting the bloom-building work into tasks, configured per tenant via bloom_planning_strategy:
split_keyspace_by_factor(default): splits the series keyspace into a fixed number of parts, controlled bybloom_split_series_keyspace_by. This is the strategy described in the sizing guidance above.split_by_series_chunks_size: sizes each task by a target amount of chunk data instead of a fixed split count, controlled bybloom_task_target_series_chunk_size.
A few additional per-tenant limits let you tune task scheduling, block encoding, and gateway prefetching:
bloom_build_max_builderslimits how many builders can work on a tenant's tasks concurrently (0allows unlimited builders).bloom_build_builder_response_timeoutsets how long the planner waits for a builder to finish a task before requeuing it (0disables the timeout).bloom_build_task_max_retrieslimits how many times a failed task is retried before it is abandoned.bloom_block_encodingsets the compression algorithm used for bloom block pages.bloom_prefetch_blocksprefetches blocks on Bloom Gateways as soon as they are built.
Refer to the per-tenant limits configuration docs for the full list of flags, defaults, and descriptions.
Bloom Gateway
Bloom Gateways handle chunks filtering requests from the index gateway. The service takes a list of chunks and a filtering expression and matches them against the blooms, filtering out those chunks not matching the given filter expression.
This component is horizontally scalable and every instance only owns a subset of the stream fingerprint range for which it performs the filtering. The sharding of the data is performed on the client side using DNS discovery of the server instances and the jumphash algorithm for consistent hashing and even distribution of the stream fingerprints across Bloom Gateway instances.
You can find all the configuration options for this component in the Configure section for the Bloom Gateways. Refer to the Enable bloom filters section above for a configuration snippet enabling this feature.
Gateway sizing and configuration
Bloom Gateways use their local file system as a Least Recently Used (LRU) cache for blooms that are downloaded from object storage. The size of the blooms depend on the ingest volume and number of unique structured metadata key-value pairs, as well as on build settings of the blooms, namely false-positive-rate. With default settings, bloom filters make up <1% of the raw structured metadata size.
Since reading blooms depends heavily on disk IOPS, Bloom Gateways should make use of multiple, locally attached SSD disks (NVMe) to increase I/O throughput.
Multiple directories on different disk mounts can be specified using the -bloom.shipper.working-directory setting when using a comma separated list of mount points, for example:
-bloom.shipper.working-directory="/mnt/data0,/mnt/data1,/mnt/data2,/mnt/data3"
Bloom Gateways need to deal with relatively large files: the bloom filter blocks.
Even though the binary format of the bloom blocks allows for reading them into memory in smaller pages, the memory consumption depends on the number of pages that are concurrently loaded into memory for processing.
The product of three settings control the maximum amount of bloom data in memory at any given time: -bloom-gateway.worker-concurrency, -bloom-gateway.block-query-concurrency, and -bloom.max-query-page-size.
Example, assuming 4 CPU cores:
-bloom-gateway.worker-concurrency=4 // 1x NUM_CORES
-bloom-gateway.block-query-concurrency=8 // 2x NUM_CORES
-bloom.max-query-page-size=64MiB
4 x 8 x 64MiB = 2048MiB
Here, the memory requirement for block processing is 2GiB. To get the minimum requirements for the Bloom Gateways, you need to double the value.
Building blooms
Bloom filters are built per stream and aggregated together into block files. Streams are assigned to blocks by their fingerprint, following the same ordering scheme as Loki’s TSDB and sharding calculation. This gives a data locality benefit when querying as streams in the same shard are likely to be in the same block.
In addition to blocks, builders maintain a list of metadata files containing references to bloom blocks and the TSDB index files they were built from. Gateways and the planner use these metadata files to discover existing blocks.
Every -bloom-build.planner.interval, the planner will load the latest TSDB files for all tenants for which bloom building is enabled, and compares the TSDB files with the latest bloom metadata files.
If there are new TSDB files or any of them have changed, the planner will create a task for the streams and chunks referenced by the TSDB file.
The builder pulls a task from the planner's queue and processes the containing streams and chunks. For a given stream, the builder will iterate through all the log lines inside its new chunks and build a bloom for the stream. In case of changes for a previously processed TSDB file, builders will try to reuse blooms from existing blocks instead of building new ones from scratch. The builder converts structured metadata from each log line of each chunk of a stream and appends the hash of each key, and key-value pair to the bloom, followed by the hashes combined with the chunk identifier. The first set of hashes allows gateways to skip whole streams, while the latter is for skipping individual chunks.
For example, given structured metadata foo=bar in the chunk c6dj8g, we append to the stream bloom the following hashes: hash("foo"), hash("foo=bar"), hash("c6dj8g" + "foo") and hash("c6dj8g" + "foo=bar").
Query sharding
Query acceleration does not just happen while processing chunks, but also happens from the query planning phase where the query frontend applies query sharding.
Loki 3.0 introduces a new per-tenant configuration flag tsdb_sharding_strategy which defaults to computing shards as in previous versions of Loki by using the index stats to come up with the closest power of two that would optimistically divide the data to process in shards of roughly the same size.
Unfortunately, the amount of data each stream has is often unbalanced with the rest, therefore, some shards end up processing more data than others.
Query acceleration introduces a new sharding strategy: bounded, which uses blooms to reduce the chunks to be processed right away during the planning phase in the query frontend, as well as evenly distributes the amount of chunks each sharded query will need to process.
Configure caches to speed up queries
Loki supports caching for query results, chunks, and index lookups to speed up query performance and reduce calls to the storage layer. Memcached is included in the Loki Helm chart and enabled by default for the chunksCache and resultsCache.
This section describes the recommended Memcached configuration to enable caching for chunks and query results.
Cache backends
Every cache in Loki (chunk_cache_config, results_cache, and so on) can use one of three interchangeable backends: Memcached, Redis, or an embedded (in-process) cache. Memcached is the backend used throughout this page and is the recommended choice for production, multi-replica deployments, because it is shared across all instances of a component. Redis is supported as an alternative external cache. The embedded cache stores data in the memory of the Loki process itself, so it isn't shared between replicas; it is best suited to single-binary or small deployments. Configure only one backend per cache. If you configure both Memcached and Redis for the same cache, Loki fails to start.
If you don't configure Memcached or Redis for a cache, Loki automatically enables the embedded cache for it, so that caching still works out of the box. This applies to the chunks cache and to the results caches. It does not apply to the index queries cache or the write dedupe cache, which stay disabled unless you configure them. Because the embedded cache is per-process, this automatic fallback is not a substitute for Memcached or Redis in a deployment with more than one replica.
For more information about which caching backend to use for each cache, refer to Configure caches and Components. For help diagnosing cache configuration problems, refer to Troubleshoot operations.
Results cache
The results cache stores results so that Loki can reuse them on subsequent queries, and it supports negative caching for log queries. It is sometimes called the frontend cache in some configurations. The results cache is consulted by query-frontends. If the cached results are incomplete, the query frontend calculates the required sub-queries and sends them further along to be executed in queriers, then also caches those results. To orchestrate all of the above, the results cache uses a query hash as the key that is computed and stored in the headers.
The results cache is really a set of six independently configurable caches under query_range, one per query type:
| Query type | Enable with | Cache config |
|---|---|---|
| Metric and log queries | cache_results |
results_cache |
| Index-stats queries | cache_index_stats_results |
index_stats_results_cache |
| Volume queries | cache_volume_results |
volume_results_cache |
| Instant metric queries | cache_instant_metric_results |
instant_metric_results_cache |
| Series queries | cache_series_results |
series_results_cache |
| Label queries | cache_label_results |
label_results_cache |
If a per-query-type cache is enabled but its cache config is left empty, Loki falls back to the results_cache configuration for that query type. This means you only need to configure results_cache to get caching for all query types, and you only need to give a query type its own cache config if you want it to use a different Memcached, Redis, or embedded cache instance. For details of each supported request type, refer to the Components section.
The index lookup cache only supports the legacy BoltDB index storage and is configured to be in-memory by default. Since moving to the TSDB indexes the attached disks/persistent volumes are utilized as cache and in-memory index lookup cache is obsolete.
Chunks cache
The chunks are cached using the chunkRef as the cache key, which is the unique reference to a chunk when it's cut in the Loki ingesters.
The chunk cache is consulted by queriers each time a set of chunkRefs are calculated to serve the query, before going to the storage layer.
Query results are significantly smaller compared to chunks. As the Loki cluster gets bigger in ingested volume, the results cache can continue to perform, whereas the chunks cache will need to grow in proportion to demand more memory. To be able to support the growing needs of a cluster, in 2023 we introduced support for memcached-extstore. Extstore is an additional feature on Memcached which supports attaching SSD disks to memcached pods to maximize their capacity.
Please see this blog post on Loki's experience with memcached-extstore for our SaaS offering, Grafana Cloud. For more information on how to tune memcached-extstore please consult the open source memcached documentation.
L2 chunks cache
As an alternative, or a complement, to memcached-extstore, Loki supports a second, "L2" chunk cache tier configured with chunk_cache_config_l2 and l2_chunk_cache_handoff. The l2_chunk_cache_handoff setting is an age threshold. Chunks younger than this age are written to and read from the primary (L1) cache that you configure in chunk_cache_config. Chunks older than this age are written to and read from the L2 cache instead. A value of 0 disables the L2 cache.
Loki does not copy or move entries between the two tiers. Each chunk is routed to one tier or the other based on its age at the time of the request. On the read path, Loki widens the threshold by 10% so that chunks near the boundary are still looked up in the L1 cache.
This lets you keep a small, fast cache of recent chunks while routing older, less frequently accessed chunks to a larger and cheaper second tier, such as a disk-backed Memcached instance.
chunk_store_config:
l2_chunk_cache_handoff: 24h
chunk_cache_config_l2:
memcached:
batch_size: 256
parallelism: 10
memcached_client:
host: <l2 chunk cache memcached host>
service: <port name of memcached service>
If the Helm chart is used, the L2 cache is configured under chunksCache.l2, which is disabled by default (chunksCache.l2.enabled: false) and defaults to a four day handoff (chunksCache.l2.l2ChunkCacheHandoff: 345600s). The Helm chart's chunksCache.l2.persistence settings can attach a persistent volume to the L2 cache pods, which is a chart-native alternative to memcached-extstore for adding disk-backed capacity.
Before you begin
- It is recommended to deploy separate Memcached type as separate components (
memcached_frontendandmemcached_chunks). - Use the Memcached image version shipped with the current release of the Loki Helm chart (
memcached.image.tag) rather than pinning to a specific version here, since the recommended version changes over time. - Consult the Loki ksonnet memcached deployment and the ksonnet memcached library.
- Index caching is not required for the TSDB index format.
- For recommendations on scaling the cache, refer to the Size the cluster page.
Steps
To enable and configure Memcached:
-
Deploy each Memcached service with at least three replicas and configure each as follows:
-
Chunk cache
--memory-limit=4096 --max-item-size=2m --conn-limit=1024 -
Query result cache
--memory-limit=1024 --max-item-size=5m --conn-limit=1024
-
-
Configure Loki to use the cache.
-
If the Helm chart is used
Set
chunksCache.addressesto the Memcached address for the chunk cache andresultsCache.addressesto the Memcached address for the query result cache, then setchunksCache.enabled=trueandresultsCache.enabled=true.Ensure that the connection limit of Memcached is at least
number_of_clients * max_idle_conns.By default, the chart also deploys and manages the Memcached servers themselves. To use your own Memcached instances instead, set
memcached.enabledtofalseto stop the chart from deploying its built-in Memcached server, keepchunksCache.enabledandresultsCache.enabledset totrueso that Loki still uses a Memcached-based cache, and point theaddressesvalues at your own service. Use the same comma-separated DNS Service Discovery format that the chart uses by default:memcached: enabled: false chunksCache: enabled: true addresses: "dnssrvnoa+_memcached-client._tcp.chunk-cache-memcached.loki.svc" resultsCache: enabled: true addresses: "dnssrvnoa+_memcached-client._tcp.results-cache-memcached.loki.svc"The Helm chart's
chunksCacheandresultsCachevalues only manage the chunk cache and the main query-range results cache. The other five results caches (index_stats_results_cache,volume_results_cache,instant_metric_results_cache,series_results_cache, andlabel_results_cache) don't have dedicated Helm values. You can still enable and configure them by adding the settings to the chart'sloki.query_rangevalue, which the chart copies into the Loki configuration file as it is written. For example:loki: query_range: cache_index_stats_results: true cache_volume_results: true -
If the Loki configuration is used, modify the following two sections in the Loki configuration file.
-
Configure the chunk cache
chunk_store_config: chunk_cache_config: memcached: batch_size: 256 parallelism: 10 memcached_client: host: <chunk cache memcached host> service: <port name of memcached service> -
Configure the query result cache
query_range: cache_results: true results_cache: cache: memcached_client: consistent_hash: true host: <memcached host> service: <port name of memcached service> max_idle_conns: 16 timeout: 200ms update_interval: 1m
-
-
Kafka-based ingestion (Experimental)
{{< admonition type="warning" >}} Kafka-based ingestion is an experimental feature. Engineering and on-call support is not available. No SLA is provided. Configuration options and metric names may change in a future release. {{< /admonition >}}
Grafana Loki can use Apache Kafka, or a Kafka-protocol-compatible system such as WarpStream, as an optional durable buffer on the write path. This topic explains what Kafka-based ingestion is, when to use it, how it works, and how to configure and monitor it.
What it is
By default, the distributor sends log streams directly to ingesters over gRPC. When you enable Kafka-based ingestion, the distributor instead (or also) writes log streams as records to a Kafka topic. Each ingester then reads, or consumes, records from one partition of that topic and processes them the same way it processes directly pushed data.
Kafka sits between the distributor and the ingester as a durable, replicated buffer. This decouples the two components so that a slow or restarting ingester doesn't block or fail writes.
Loki uses the franz-go Kafka client library. If you need to debug Kafka client behavior in your logs, look for the component name kafka_client. Kafka client metrics use the loki_kafka_client_ prefix.
When and why to use it
Consider enabling Kafka-based ingestion if you want to:
- Decouple ingestion durability from ingester availability.
- Absorb ingester restarts and rolling updates without blocking or failing writes.
- Automatically replay buffered data from the last committed offset when an ingester restarts, instead of relying only on the ingester's write-ahead log (WAL).
- Scale write throughput independently of ingester replica count, by adding Kafka partitions.
Kafka-based ingestion also underpins the ingest-limits service and the experimental data object builders used for columnar storage. These features require Kafka even if you don't otherwise need the durability benefits above.
If you don't have an existing Kafka deployment and don't need these benefits, you can continue to use the default gRPC write path. Enabling Kafka adds an operational dependency: you must run, or pay for, a Kafka cluster (or a compatible managed service) and monitor it.
Architecture summary
{{< mermaid >}} flowchart LR A[Distributor] -->|produces records| B[(Kafka topic, partitioned)] B -->|partition 0| C1[Ingester A] B -->|partition 1| C2[Ingester B] B -->|partition N| C3[Ingester N] D[Partition ring] -.assigns partition ownership.-> C1 D -.-> C2 D -.-> C3 {{< /mermaid >}}
The distributor writes each stream as one or more Kafka records to a topic. The topic has multiple partitions. Each ingester owns exactly one partition and consumes records only from that partition. Ingesters use the partition ring to coordinate which ingester owns which partition.
Every ingester process always registers in the classic hash ring it uses for gRPC writes, whether or not Kafka-based ingestion is enabled. When you also enable ingester.kafka_ingestion.enabled, that same process additionally joins the partition ring. Loki supports two ways to deploy this:
Because the two rings assign ownership of a tenant's streams differently (token-hash with replication, compared to shuffle-sharded partitions with a single owner), the querier has to be told explicitly which ring to use when it looks up ingesters. See Read path and querier.query_partition_ingesters under Configuration overview.
{{< admonition type="note" >}} You can use any system that implements the Kafka wire protocol, such as WarpStream, instead of running Apache Kafka yourself. Loki only needs the broker address and, optionally, SASL credentials. No code changes are required. {{< /admonition >}}
Read path
Enabling Kafka-based ingestion primarily changes the write path. LogQL queries still read recent data from ingester memory and older data from long-term object storage, the same way they do when Kafka-based ingestion is disabled.
However, the querier must be told which ring to use to find the ingesters that hold recent data: the classic hash ring (the default), or the partition ring that Kafka-based ingestion uses. Set querier.query_partition_ingesters: true to switch the querier, and any locally evaluating rulers, over to the partition ring. Until you set this, the querier keeps using the classic ring even if the distributor and ingesters are fully configured for Kafka-based ingestion. For the full migration sequence and when to flip this setting, refer to Migrate to Kafka-based ingestion.
Configuration overview
This section groups the relevant configuration blocks at a high level. For the full list of fields and their defaults, refer to the Configuration reference.
kafka_config: the Kafka client configuration shared by every Loki component that reads or writes Kafka records. It includes the broker addresses (reader_config.addressandwriter_config.address), the topic name, SASL credentials, producer tuning (producer_linger,producer_max_inflight_requests_per_broker,producer_max_record_size_bytes), and consumer tuning (max_consumer_lag_at_startup,consumer_group_offset_commit_interval,max_consumer_workers).distributor.kafka_writes_enabledanddistributor.ingester_writes_enabled: control whether the distributor writes to Kafka, to ingesters over gRPC, or both. At least one must betrue.ingester.kafka_ingestion: controls whether ingesters consume from Kafka, and configures the partition ring key-value store that ingesters, distributors, queriers, and rulers use to coordinate partition ownership.querier.query_partition_ingesters: controls whether the querier, and any ruler evaluating rules locally, looks up ingesters using the partition ring instead of the classic hash ring. Set this totrueonly after the Kafka-consuming ingesters have caught up and held data for at leastquerier.query_ingesters_within(default: 3 hours); otherwise recent queries can return incomplete results. Defaults tofalse.limits_config.ingestion_partitions_tenant_shard_size: the number of Kafka partitions a single tenant's data is shuffle-sharded across. The default,0, means a tenant's data uses all partitions.ingest_limits: configuration for the optional ingest-limits service, which uses its own Kafka topic, partition count, and consumer group to track stream metadata.
Authentication limitations
The only Kafka authentication mechanism Loki supports today is SASL PLAIN, configured with kafka_config.sasl_username and kafka_config.sasl_password. Both fields must be set together, or both left empty.
Loki has no built-in Transport Layer Security (TLS) or mutual TLS (mTLS) configuration for the Kafka connection. If your organization requires an encrypted connection to Kafka, you must terminate TLS at the network layer, for example with a sidecar proxy or a load balancer in front of your Kafka brokers.
Partition sizing guidance
The number of Kafka partitions in your topic limits how many ingesters can actively consume from it, because each ingester owns exactly one partition.
Keep the following in mind when you choose a partition count:
kafka_config.auto_create_topic_default_partitionsdefaults to1000. This is a cluster-wide Kafka broker setting (equivalent to Kafka'snum.partitions), not a per-topic setting, and it only takes effect if you let Loki auto-create the topic.- The partition count sets the maximum number of ingesters that can actively consume from the topic at once. If you have more ingesters than partitions, the extra ingesters have no partition to own and stay idle for Kafka-based ingestion.
limits_config.ingestion_partitions_tenant_shard_sizecontrols how many partitions a single tenant's streams are shuffle-sharded across. The default,0, disables shuffle sharding, so a tenant's data spreads across all partitions.- Start with more partitions than you expect to need. Partitions are inexpensive to create in Kafka, but reducing the partition count of an existing topic later is difficult and usually requires creating a new topic.
Offset management and restart behavior
When Kafka-based ingestion is enabled, each ingester follows this sequence to safely resume consuming after a restart:
- On startup, the ingester fetches the last offset it committed for its partition.
- If no committed offset exists, for example on first startup, the ingester starts from the earliest available offset.
- The ingester replays Kafka records from that offset until its consumption lag is within
kafka_config.max_consumer_lag_at_startup(default: 15 seconds). - Only after catching up does the ingester mark itself
ACTIVEin the partition ring and pass its readiness check. - During normal operation, the ingester commits its current offset to Kafka every
kafka_config.consumer_group_offset_commit_interval(default: 1 second).
Because of this sequence, ingester restarts are safe: any data produced to Kafka while an ingester was down is still in the topic, and the ingester automatically replays it after it comes back up.
Monitoring Kafka ingestion
This is the full list of Kafka-related metrics, organized by component. For the highest-signal subset of these metrics and example alerting queries, refer to Key metrics for monitoring Loki.
Distributor (producer side):
loki_distributor_kafka_appends_total(labels:partition,status): total number of Kafka produce attempts.loki_distributor_kafka_latency_seconds: end-to-end produce latency.loki_distributor_kafka_sent_bytes_total: bytes written to Kafka.loki_distributor_kafka_records_per_write_request: how many Kafka records each incoming push request was split into.
Ingester (consumer side):
loki_ingester_partition_records_batch_process_duration_seconds: time to process a batch of Kafka records.loki_ingester_partition_current_offset: the offset the ingester has most recently consumed. Useful for lag alerting.loki_ingester_partition_push_latency_seconds: latency of the internal push after consuming a record from Kafka.loki_ingester_partition_consume_workers_count: number of consumer worker goroutines.
Kafka client internals (reader and writer):
loki_kafka_client_partition_reader_consumption_lag_seconds(labels:phase,partition_state): estimated time the reader is behind the latest produced record. Alert if this grows over time.loki_kafka_client_partition_reader_fetch_errors_total: errors fetching records from Kafka brokers.loki_kafka_client_partition_reader_fetches_totalandloki_kafka_client_partition_reader_records_per_fetch: fetch throughput.loki_kafka_client_partition_reader_offset_commit_requests_totalandloki_kafka_client_partition_reader_offset_commit_failures_total: health of offset commits to Kafka.loki_kafka_client_produce_write_wait_seconds,loki_kafka_client_produce_write_time_seconds,loki_kafka_client_produce_read_wait_secondsandloki_kafka_client_produce_read_time_seconds(labels:node_id,host): native histograms of how long each stage of a Produce request to each broker takes: waiting to be written, writing, waiting for the response, and reading the response.
Configuration examples
Dual-write for a safe rollout
Write to both Kafka and ingesters at the same time so you can validate Kafka-based ingestion before relying on it. Queries still use the classic hash ring at this stage (querier.query_partition_ingesters stays false).
distributor:
kafka_writes_enabled: true
ingester_writes_enabled: true
ingester:
kafka_ingestion:
enabled: true
kafka_config:
topic: loki-logs
reader_config:
address: kafka:9092
writer_config:
address: kafka:9092
Kafka-only write path
Once you've validated dual-write and switched queriers to the partition ring, you can disable the direct gRPC writes to ingesters. Refer to Migrate to Kafka-based ingestion for the full, safely ordered sequence.
distributor:
kafka_writes_enabled: true
ingester_writes_enabled: false
ingester:
kafka_ingestion:
enabled: true
querier:
query_partition_ingesters: true
kafka_config:
topic: loki-logs
reader_config:
address: kafka:9092
writer_config:
address: kafka:9092
Managed Kafka with SASL authentication
This example connects to a managed Kafka-protocol-compatible service, such as WarpStream, using SASL PLAIN authentication and separate reader and writer broker addresses.
distributor:
kafka_writes_enabled: true
ingester_writes_enabled: true
ingester:
kafka_ingestion:
enabled: true
kafka_config:
topic: loki-logs
reader_config:
address: reader.kafka.example.com:9092
writer_config:
address: writer.kafka.example.com:9092
sasl_username: ${KAFKA_SASL_USERNAME}
sasl_password: ${KAFKA_SASL_PASSWORD}
Links
Use label-based access control with Loki
{{< admonition type="note" >}} This feature is generally available from v4.0. For the latest releases, refer to the Release notes. {{< /admonition >}}
Label-based access control (LBAC) restricts the logs a tenant can query to those that match one or more Prometheus label selectors. You associate a set of selectors with a tenant, and queries from that tenant only return data from log streams that match at least one of the selectors. Because selectors can be combined into "OR" groups, where each group is a list of things joined by "AND" you can express any required policy. For example, (A and B and C) or (D and E) or (F).
LBAC builds on Loki's multi-tenancy: policies are scoped per tenant, so you must run Loki in multi-tenant mode. For more information, refer to the multi-tenancy documentation.
Why use LBAC
- Restrict a tenant to a subset of its own data. For example, one team should only see logs from its own Kubernetes namespace, even though the tenant contains logs from many namespaces.
- Exclude sensitive log streams from some users. For example, you can hide streams labeled
secret="true"from most users, while still letting a smaller group of users query them. - Enforce access boundaries per tenant without creating a separate Loki tenant for every team. Running one tenant per team adds operational overhead. LBAC lets you keep a single tenant and instead scope access with label policies.
Security model
Loki does not include its own authentication layer. Both the tenant (X-Scope-OrgID) and the label policy (X-Prom-Label-Policy) are read from trusted HTTP headers on incoming requests.
{{< admonition type="warning" >}}
Never expose the Loki API directly to clients. A client that can reach Loki can set the X-Scope-OrgID and X-Prom-Label-Policy headers itself and bypass label-based access control entirely. You must run an authentication gateway in front of Loki that authenticates the request, sets the tenant, and attaches the correct label policy.
{{< /admonition >}}
The gateway is responsible for stripping any user supplied X-Scope-OrgID and X-Prom-Label-Policy and replacing them with values it derives from the authenticated identity. One open-source gateway that sets these headers is db-auth-gateway. For general guidance on putting an authenticating reverse proxy in front of Loki, refer to the authentication documentation.
How LBAC works
At a high level, your gateway attaches a label policy to each request, and Loki carries that policy through the read path so that every component filters data to match it.
- Your authentication gateway authenticates the request and sets the
X-Prom-Label-PolicyHTTP header. Each value in the header has the form<TENANT>:<URL-ENCODED SELECTOR>, for exampletenant1:%7Benv%3D%22dev%22%7Dfor the selector{env="dev"}. - An HTTP middleware inside Loki parses this header on every incoming request and stores the resulting policies in the request context.
- As the request moves between Loki's internal components, for example from the query frontend to the queriers, ingesters, and index gateway, the policies travel with it as gRPC metadata. This means the same policy applies no matter which component ultimately reads the data.
- Loki enforces the policy at several points in the read path:
- It filters chunks and streams while running the query, for both the original query engine and the newer query engine.
- It filters the results of label-values queries against the store and the ingesters.
- In the query frontend, it filters the response of volume queries, and it rewrites aggregated-metrics queries so they only return permitted data.
The following diagram shows the request flow for a query from a client through the gateway and into Loki:
sequenceDiagram
participant Client
participant Gateway as Authentication gateway
participant QF as Query frontend
participant Q as Querier / Ingester
Client->>Gateway: Query request
Gateway->>Gateway: Authenticate request
Gateway->>QF: Query request + X-Scope-OrgID + X-Prom-Label-Policy
QF->>Q: Query request (policy propagated over gRPC)
Q->>Q: Filter streams and chunks by policy
Q->>QF: Filtered results
QF->>Gateway: Filtered response
Gateway->>Client: Filtered response
LBAC and ingestion
Label-based access control is not enforced on write (push) requests. It only restricts what a tenant can read. A tenant that is allowed to write can push log lines with any labels, regardless of the label policy that applies to its queries.
LBAC and alertmanager and ruler
Label policies are not enforced by the Alertmanager or ruler HTTP endpoints. This means that the requests they serve contain everything for a particular tenant without applying label-based access control. For example, listing all rule groups in the ruler returns all rule groups for the tenant, even if a label selector in the policy would exclude some of the labels on the rules. However it does apply when you query the metrics those components generate, such as ALERTS.
LBAC and querying
LogQL queries use the following format:
{ log stream selector } | log pipeline
LBAC only applies to the log stream selector portion of a query, that is the part of the query that comes before the pipe (|). It does not apply to label filter expressions or other parts of the log pipeline.
For more information, refer to the Log stream selector and Label filter expression sections in the documentation about Log queries.
Enable label-based access control
LBAC requires multi-tenant mode, which is the default. Ensure authentication is enabled so that requests carry a tenant:
auth_enabled: true
Then enable LBAC:
lbac:
enabled: true
This can also be set with the -lbac.enabled command-line flag. When enabled, Loki parses the X-Prom-Label-Policy header on incoming requests and enforces the policies it contains.
Configuration reference
LBAC currently has a single configuration setting. There are no per-tenant settings in Loki's YAML configuration: policies for each tenant arrive dynamically in the X-Prom-Label-Policy header, set by your authentication gateway, not through Loki's configuration file.
lbac:
# Enables label based access control through the X-Prom-Label-Policy header.
[enabled: <boolean> | default = false]
| Setting | CLI flag | Default | Description |
|---|---|---|---|
lbac.enabled |
-lbac.enabled |
false |
Enables label-based access control through the X-Prom-Label-Policy header. This setting is experimental. |
Setting up a label policy
A label policy is conveyed to Loki in the X-Prom-Label-Policy request header. This header is set by your authentication gateway, not by clients. Each header value has the form:
<TENANT>:<URL-ENCODED SELECTOR>
The selector is a standard label matcher set such as {env="dev"}, URL-encoded so that it is safe to carry in a header. For example, the policy {env="dev"} for tenant tenant1 is encoded as:
X-Prom-Label-Policy: tenant1:%7Benv%3D%22dev%22%7D
To associate multiple selectors with a tenant, set multiple values. You can either repeat the header or separate the encoded policies with a comma (,). The selectors are combined with OR into groups where each group is a list of things joined by AND: a stream is returned if any of the streams matches any one of the selector groups. For example, a policy written {a='true', b='true, c='true'} OR {x='true', y='true'} reads as {a='true' AND b='true'AND c='true'} OR {x='true'AND y='true'}.
Exclude a label
One common use case for an LBAC policy is to exclude logs that have a specific label. For example, to exclude all log lines with the label secret=true, use a selector with secret!="true":
{secret!="true"}
Use multiple selectors
To allow access to both the production and development environments while excluding logs with the label secret=true in the production environment, use multiple selectors:
{secret!="true", env="prod"}
{env="dev"}
These selectors enforce the policy as follows:
{secret!="true", env="prod"}matches and returns log lines from the production environment that do not have thesecret: truelabel.{env="dev"}matches and returns log lines from the development environment, even if they have thesecret: truelabel.
Real-world examples
The following examples use labels that Loki assigns by default when logs are collected through OpenTelemetry, such as service_name, k8s_namespace_name, and deployment_environment_name. For the full list of default OpenTelemetry-derived labels, refer to Default labels. Each example shows the LogQL selector and the resulting X-Prom-Label-Policy header value that your gateway would set, following the same format as Setting up a label policy.
Restrict a team to one namespace
Scope a tenant so that it can only see logs from the checkout Kubernetes namespace. This is useful when a platform team gives a single application team access to a shared tenant, but the application team should only see its own namespace.
Selector:
{k8s_namespace_name="checkout"}
Header:
X-Prom-Label-Policy: team-checkout:%7Bk8s_namespace_name%3D%22checkout%22%7D
Restrict by environment
Give separate policies to a production support team and a staging support team that share the same tenant.
Production team selector:
{deployment_environment_name="production"}
Header:
X-Prom-Label-Policy: team-prod:%7Bdeployment_environment_name%3D%22production%22%7D
Staging team selector:
{deployment_environment_name="staging"}
Header:
X-Prom-Label-Policy: team-staging:%7Bdeployment_environment_name%3D%22staging%22%7D
Restrict by service, with an exclusion
Combine a service restriction with an exclusion, similar to Use multiple selectors, but scoped to a specific service.
Allow access to the payments-api service, but exclude any of its streams labeled secret="true", and separately allow access to every other service:
{service_name="payments-api", secret!="true"}
{service_name!="payments-api"}
Header:
X-Prom-Label-Policy: team-payments:%7Bservice_name%3D%22payments-api%22%2C%20secret%21%3D%22true%22%7D,team-payments:%7Bservice_name%21%3D%22payments-api%22%7D
Restrict by region for a distributed team
Scope a regional on-call team so that it only sees logs from its own region.
Selector:
{cloud_region="us-west-1"}
Header:
X-Prom-Label-Policy: team-uswest:%7Bcloud_region%3D%22us-west-1%22%7D
Combine selectors across multiple namespaces
Grant a tenant access to logs from two Kubernetes namespaces by giving it two separate selectors for the same tenant:
{k8s_namespace_name="frontend"}
{k8s_namespace_name="checkout"}
Because a tenant's selectors are combined with OR, a stream is returned if it matches either one: the frontend namespace or the checkout namespace.
Header:
X-Prom-Label-Policy: team-multi:%7Bk8s_namespace_name%3D%22frontend%22%7D,team-multi:%7Bk8s_namespace_name%3D%22checkout%22%7D
Manage tenant isolation
Grafana Loki is a multi-tenant system; requests and data for tenant A are isolated from
tenant B. Requests to the Loki API should include an HTTP header
(X-Scope-OrgID) that identifies the tenant for the request.
Tenant IDs must not be longer than 150 bytes and can only include a specific set of characters; see Restrictions below for the full rules. Operators are recommended to use a reasonable limit for uniquely identifying tenants; 20 bytes is usually enough.
Loki defaults to running in multi-tenant mode.
Multi-tenant mode is set in the configuration with auth_enabled: true, or with the equivalent command line flag -auth.enabled (default true).
When configured with auth_enabled: false, Loki uses a single tenant.
The X-Scope-OrgID header is not required in Loki API requests.
The single tenant ID defaults to fake and can be changed via the no_auth_tenant configuration option.
On a fresh cluster with an empty bucket this is safe to change at any time; on a cluster that has already
written data, changing the tenant ID requires migrating existing data to the new tenant path (see Loki Migrate Tool).
When auth_enabled: true, a request that does not include the X-Scope-OrgID header fails with an HTTP 401 Unauthorized error and the message no org id.
For more information about setting this header, including examples of configuring a reverse proxy to add it, refer to Manage authentication.
To troubleshoot this error, refer to No org ID.
Multi-tenant Queries
In multi-tenant mode, queries may gather results from multiple tenants.
Set the querier configuration option multi_tenant_queries_enabled: true, or the equivalent command line flag -querier.multi-tenant-queries-enabled (default false), to enable queries across tenants.
The query API request defines the tenants.
Specify multiple tenants
in the query request HTTP header X-Scope-OrgID by separating the tenant IDs with the pipe character (|).
For example, a query for tenants A and B requires the header X-Scope-OrgID: A|B.
Only query endpoints support multi-tenant calls.
Calls to GET /loki/api/v1/tail, POST /loki/api/v1/push, and POST /otlp/v1/logs will return an HTTP 400 error if more than one tenant is defined in the HTTP header.
Instant and range queries support label filtering using tenant IDs. For example, the query
{app="foo", __tenant_id__=~"a.+"} | logfmt
will return results for all tenants
that have a tenant ID that begins with the character a.
The __tenant_id__ label is not stored with the log data.
Loki adds it to the results only when a query spans more than one tenant.
If the request names a single tenant, the results do not include this label.
If the label __tenant_id__ is already present in a log stream, it is prepended with the string original_.
Tenant ID filtering in stages is not supported. An example of a query that will not work:
{app="foo"} | __tenant_id__="1" | logfmt
Restrictions
Tenant IDs must not be longer than 150 bytes and can only include the following characters:
- Alphanumeric characters
0-9a-zA-Z
- Special characters
- Exclamation point (
!) - Hyphen (
-) - Underscore (
_) - Single period (
.) - Asterisk (
*) - Single quote (
') - Open parenthesis (
() - Close parenthesis (
))
- Exclamation point (
{{< admonition type="note" >}}
For security reasons, . and .. aren't valid tenant IDs.
{{< /admonition >}}
{{< admonition type="warning" >}}
Do not include a colon (:) in the X-Scope-OrgID header.
A colon is not a valid tenant ID character.
Most Loki components stop reading the tenant ID at the first colon and ignore the rest of the value, and the ignored part is not checked against the rules on this page.
Other components read the whole header value instead, so a header that contains a colon can produce inconsistent results.
{{< /admonition >}}
Monitor tenant limits using the Overrides Exporter
Loki is a multi-tenant system that supports applying limits to each tenant as a mechanism for resource management. The overrides-exporter module exposes these limits as Prometheus metrics in order to help operators better understand tenant behavior.
Context
Configuration updates to tenant limits can be applied to Loki without restart via the runtime_config feature.
Example
The overrides-exporter module is disabled by default. We recommend running a single instance per cluster to avoid issues with metric cardinality. The overrides-exporter creates one metric for every scalar field in the limits configuration under the metric loki_overrides_defaults with the default value for that field after loading the Loki configuration. It also exposes another metric for every differing field for every tenant.
Using an example runtime.yaml:
overrides:
"tenant_1":
ingestion_rate_mb: 10
max_streams_per_user: 100000
max_chunks_per_query: 100000
Launch an instance of the overrides-exporter:
loki -target=overrides-exporter -runtime-config.file=runtime.yaml -config.file=basic_schema_config.yaml -server.http-listen-port=8080
To inspect the tenant limit overrides:
$ curl -sq localhost:8080/metrics | grep override
# HELP loki_overrides Resource limit overrides applied to tenants
# TYPE loki_overrides gauge
loki_overrides{limit_name="ingestion_rate_mb",user="tenant_1"} 10
loki_overrides{limit_name="max_chunks_per_query",user="tenant_1"} 100000
loki_overrides{limit_name="max_streams_per_user",user="tenant_1"} 100000
# HELP loki_overrides_defaults Default values for resource limit overrides applied to tenants
# TYPE loki_overrides_defaults gauge
loki_overrides_defaults{limit_name="block_ingestion_status_code"} 260
loki_overrides_defaults{limit_name="cardinality_limit"} 100000
loki_overrides_defaults{limit_name="creation_grace_period"} 6e+11
loki_overrides_defaults{limit_name="ingestion_burst_size_mb"} 6
loki_overrides_defaults{limit_name="ingestion_rate_mb"} 4
loki_overrides_defaults{limit_name="max_cache_freshness_per_query"} 6e+11
loki_overrides_defaults{limit_name="max_chunks_per_query"} 2e+06
loki_overrides_defaults{limit_name="max_concurrent_tail_requests"} 10
loki_overrides_defaults{limit_name="max_entries_limit_per_query"} 5000
loki_overrides_defaults{limit_name="max_global_streams_per_user"} 5000
loki_overrides_defaults{limit_name="max_label_name_length"} 1024
loki_overrides_defaults{limit_name="max_label_names_per_series"} 15
loki_overrides_defaults{limit_name="max_label_value_length"} 2048
loki_overrides_defaults{limit_name="max_line_size"} 262144
loki_overrides_defaults{limit_name="max_metadata_cache_freshness"} 8.64e+13
loki_overrides_defaults{limit_name="max_queriers_per_tenant"} 0
loki_overrides_defaults{limit_name="max_query_capacity"} 0
loki_overrides_defaults{limit_name="max_query_length"} 2.5956e+15
loki_overrides_defaults{limit_name="max_query_lookback"} 0
loki_overrides_defaults{limit_name="max_query_parallelism"} 32
loki_overrides_defaults{limit_name="max_query_series"} 500
loki_overrides_defaults{limit_name="max_streams_matchers_per_query"} 1000
loki_overrides_defaults{limit_name="max_streams_per_user"} 0
loki_overrides_defaults{limit_name="max_structured_metadata_size"} 65536
loki_overrides_defaults{limit_name="min_sharding_lookback"} 0
loki_overrides_defaults{limit_name="per_stream_rate_limit"} 3.145728e+06
loki_overrides_defaults{limit_name="per_stream_rate_limit_burst"} 1.572864e+07
loki_overrides_defaults{limit_name="per_tenant_override_period"} 1e+10
loki_overrides_defaults{limit_name="query_timeout"} 6e+10
loki_overrides_defaults{limit_name="reject_old_samples_max_age"} 6.048e+14
loki_overrides_defaults{limit_name="retention_period"} 0
loki_overrides_defaults{limit_name="ruler_max_rule_groups_per_tenant"} 0
loki_overrides_defaults{limit_name="ruler_max_rules_per_rule_group"} 0
loki_overrides_defaults{limit_name="ruler_remote_write_queue_batch_send_deadline"} 0
loki_overrides_defaults{limit_name="ruler_remote_write_queue_capacity"} 0
loki_overrides_defaults{limit_name="ruler_remote_write_queue_max_backoff"} 0
loki_overrides_defaults{limit_name="ruler_remote_write_queue_max_samples_per_send"} 0
loki_overrides_defaults{limit_name="ruler_remote_write_queue_max_shards"} 0
loki_overrides_defaults{limit_name="ruler_remote_write_queue_min_backoff"} 0
loki_overrides_defaults{limit_name="ruler_remote_write_queue_min_shards"} 0
loki_overrides_defaults{limit_name="ruler_remote_write_timeout"} 0
loki_overrides_defaults{limit_name="split_queries_by_interval"} 3.6e+12
loki_overrides_defaults{limit_name="tsdb_max_bytes_per_shard"} 6.291456e+08
loki_overrides_defaults{limit_name="tsdb_max_query_parallelism"} 128
loki_overrides_defaults{limit_name="volume_enabled"} 1
loki_overrides_defaults{limit_name="volume_max_series"} 1000
{{< admonition type="note" >}}
This output is a partial example. The exact set of metrics depends on your Loki version, since loki_overrides_defaults includes one series for every scalar field in the limits_config block, and that block grows with each release. Query your own instance's /metrics endpoint for the authoritative, current list rather than relying on a fixed example.
{{< /admonition >}}
Alerts can be created based on these metrics to inform operators when tenants are close to hitting their limits allowing for increases to be applied before the tenant limits are exceeded.
Manage recording rules
Recording rules are queries that run in an interval and produce metrics from logs that can be pushed to a Prometheus compatible backend.
Recording rules are evaluated by the ruler component. Each ruler acts as its own querier, in the sense that it
executes queries against the store without using the query-frontend or querier components. It will respect all query
limits put in place for the querier.
The Loki implementation of recording rules largely reuses Prometheus' code.
Samples generated by recording rules are sent to Prometheus using Prometheus' remote-write feature.
Write-Ahead Log (WAL)
All samples generated by recording rules are written to a WAL. The WALs main benefit is that it persists the samples
generated by recording rules to disk, which means that if your ruler crashes, you won't lose any data.
We are trading off extra memory usage and slower start-up times for this functionality.
A WAL is created per tenant; this is done to prevent cross-tenant interactions. If all samples were to be written to a single WAL, this would increase the chances that one tenant could cause data-loss for others. A typical scenario here is that Prometheus will, for example, reject a remote-write request with 100 samples if just 1 of those samples is invalid in some way.
Start-up
When the ruler starts up, it deletes (wipes) its remote-write WAL directory and starts each tenant with a fresh, empty
WAL. WAL data left over from a previous run is not replayed, so any samples that had not yet been flushed to remote storage
before the restart are discarded.
Truncation
WAL files are regularly truncated to reduce their size on disk. This guide from one of the Prometheus maintainers (Ganesh Vernekar) gives an excellent overview of the truncation, checkpointing, and replaying of the WAL.
Cleaner
The WAL Cleaner watches for abandoned WALs (tenants who no longer have recording rules associated) and deletes them. Enable this feature only if you are running into storage concerns with WALs that are too large. WALs should not grow excessively large due to truncation.
The WAL Cleaner is disabled by default. Enable it by setting -ruler.wal-cleaner.period to a non-zero value.
Scaling
See Mimir's guide for configuring Grafana Mimir hash rings for scaling the ruler using a ring.
{{< admonition type="note" >}}
By default, the ruler shards by rule group, not by individual rules. This is an artifact of the fact that Prometheus
recording rules need to run in order since one recording rule can reuse another, but this is not possible in Loki.
To shard by individual rule instead, set -ruler.sharding-algo="by-rule". For details, refer to Tuning.
{{< /admonition >}}
Deployment
The ruler needs to persist its WAL files to disk, and it incurs a bit of a start-up cost by reading these WALs into memory.
As such, it is recommended that you try to minimize churn of individual ruler instances, since recording rule samples
produced while a tenant's WAL is being read from disk are dropped rather than persisted (see Start-up).
Kubernetes
It is recommended that you run the rulers using StatefulSets. The ruler will write its WAL files to persistent storage,
so a Persistent Volume should be utilized.
Remote-Write
Client configuration
Remote-write client configuration is fully compatible with prometheus configuration format.
remote_write:
clients:
mimir:
url: http://mimir/api/v1/push
write_relabel_configs:
- action: replace
target_label: job
replacement: loki-recording-rules
Per-Tenant Limits
Remote-write can be configured at a global level in the base configuration, and certain parameters tuned specifically on a per-tenant basis. Most of the configuration options defined here have override options (which can be also applied at runtime!).
Tuning
Remote-write can be tuned if the default configuration is insufficient (see Failure Modes below).
There is a guide on the Prometheus website, all of which applies to Loki, too.
Rules can be evenly distributed across available rulers by using -ruler.enable-sharding=true and -ruler.sharding-algo="by-rule".
-ruler.enable-sharding and -ruler.sharding-strategy control which ruler instances own which tenants (ring-based
sharding, with optional shuffle sharding across rulers). For details, refer to
Shuffle sharding.
-ruler.sharding-algo controls how a single tenant's rule groups are split up for evaluation once sharding is
enabled. Rule groups execute in order; this is a feature inherited from Prometheus' rule engine (which Loki uses), but
Loki has no need for this constraint because rules cannot depend on each other. The default algorithm, by-group,
shards by rule group, but this may be undesirable, since some rule groups could contain more expensive rules, which
can lead to subsequent rules missing evaluations. The by-rule algorithm creates one rule group for each rule the
ruler instance "owns" (based on its hash ring), and these groups are all executed concurrently.
Observability
Since Loki reuses the Prometheus code for recording rules and WALs, it also gains all of Prometheus' observability.
Prometheus exposes a number of metrics for its WAL implementation, and these have all been prefixed with loki_ruler_wal_.
For example: prometheus_remote_storage_bytes_total → loki_ruler_wal_prometheus_remote_storage_bytes_total
Additional metrics are exposed, also with the prefix loki_ruler_wal_. All per-tenant metrics contain a tenant
label, so be aware that cardinality could begin to be a concern if the number of tenants grows sufficiently large.
Some key metrics to note are:
loki_ruler_wal_appender_ready: whether a WAL appender is ready to accept samples (1) or not (0)loki_ruler_wal_prometheus_remote_storage_samples_total: number of samples sent per tenant to remote storageloki_ruler_wal_prometheus_remote_storage_samples_pending: samples currently buffered in memory, waiting to be sent to remote storage. This is a gauge, not a counter, so don't userate()orincrease()with it.loki_ruler_wal_prometheus_remote_storage_samples_failed_total: samples that failed when sent to remote storageloki_ruler_wal_prometheus_remote_storage_samples_dropped_total: samples dropped by relabel configurationsloki_ruler_wal_prometheus_remote_storage_samples_retried_total: samples re-resent to remote storage
loki_ruler_wal_prometheus_remote_storage_highest_timestamp_in_seconds: highest timestamp of sample appended to WALloki_ruler_wal_prometheus_remote_storage_queue_highest_sent_timestamp_seconds: highest timestamp of sample sent to remote storage.
We've created a basic dashboard in our loki-mixin which you can use to administer recording rules.
Failure Modes
Remote-Write Lagging
Remote-write can lag behind for many reasons:
- Remote-write storage (Prometheus) is temporarily unavailable
- A tenant is producing samples too quickly from a recording rule
- Remote-write is tuned too low, creating backpressure
It can be determined by subtracting
loki_ruler_wal_prometheus_remote_storage_queue_highest_sent_timestamp_seconds from
loki_ruler_wal_prometheus_remote_storage_highest_timestamp_in_seconds.
In case 1, the ruler will continue to retry sending these samples until the remote storage becomes available again. Be
aware that if the remote storage is down for longer than ruler.wal.max-age, data loss may occur after truncation occurs.
In cases 2 and 3, you should consider tuning remote-write appropriately.
Further reading: see this blog post by Prometheus maintainer Callum Styan.
Appender Not Ready
Each tenant's WAL has an "appender" internally; this appender is used to append samples to the WAL. The appender is marked
as not ready until the WAL storage has been initialized upon startup. You can alert on loki_ruler_wal_appender_ready < 1
to detect tenants whose WAL is not yet ready to accept samples.
Corrupt WAL
If a disk fails or the ruler does not terminate correctly, there's a chance one or more tenant WALs can become corrupted.
The ruler wipes its WAL directory on startup, so any WAL left over from a previous run — corrupt or not — is discarded and
recreated fresh rather than replayed.
Found another failure mode?
Open an issue and tell us about it!
Enforce rate limits and push request validation
Loki will reject requests if they exceed a usage threshold (rate limit error) or if they are invalid (validation error).
All occurrences of these errors can be observed using the loki_discarded_samples_total and loki_discarded_bytes_total metrics. The sections below describe the various possible reasons specified in the reason label of these metrics.
It is recommended that Loki operators set up alerts or dashboards with these metrics to detect when rate limits or validation errors occur.
Terminology
- sample: a log line with structured metadata
- stream: samples with a unique combination of labels
- active stream: streams that are present in the ingesters - these have recently received log lines within the
chunk_idle_periodperiod (default: 30m)
Rate-Limit Errors
Rate-limits are enforced when Loki cannot handle more requests from a tenant.
Rate-limit errors reject the whole request before Loki stores any of it. The tables below list Sample discarded: No for these errors because Loki never accepts the samples in the first place, but the request still increments loki_discarded_samples_total and loki_discarded_bytes_total. Because these errors are retryable, a client can resend the same data after the rate limit condition clears.
rate_limited
This rate limit is enforced when a tenant has exceeded their configured log ingestion rate limit.
One solution if you're seeing samples dropped due to rate_limited is simply to increase the rate limits on your Loki cluster. These limits can be modified globally in the limits_config block, or on a per-tenant basis in the runtime overrides file. The config options to use are ingestion_rate_mb and ingestion_burst_size_mb.
Note that you'll want to make sure your Loki cluster has sufficient resources provisioned to be able to accommodate these higher limits. Otherwise your cluster may experience performance degradation as it tries to handle this higher volume of log lines to ingest.
Another option to address samples being dropped due to rate_limits is simply to decrease the rate of log lines being sent to your Loki cluster. Consider collecting logs from fewer targets or setting up drop stages in Alloy to filter out certain log lines. You can also use Alloy's rate limiting to control the volume of logs sent to your Loki cluster.
| Property | Value |
|---|---|
| Enforced by | distributor |
| Outcome | Request rejected |
| Retryable | Yes |
| Sample discarded | No |
| Configurable per tenant | Yes |
| HTTP status code | 429 Too Many Requests |
per_stream_rate_limit
This limit is enforced when a single stream reaches its rate limit.
Each stream has a rate limit applied to it to prevent individual streams from overwhelming the set of ingesters it is distributed to (the size of that set is equal to the replication_factor value).
This value can be modified globally in the limits_config block, or on a per-tenant basis in the runtime overrides file. The config options to adjust are per_stream_rate_limit and per_stream_rate_limit_burst. The default values are 3MB for per_stream_rate_limit and 15MB for per_stream_rate_limit_burst.
Another option you could consider to decrease the rate of samples dropped due to per_stream_rate_limit is to split the stream that is getting rate limited into several smaller streams. A third option is to use the Alloy stage.limit block to limit the rate of samples sent to the stream hitting the per_stream_rate_limit.
We typically recommend setting per_stream_rate_limit no higher than 5MB, and per_stream_rate_limit_burst no higher than 20MB.
| Property | Value |
|---|---|
| Enforced by | ingester |
| Outcome | Request rejected |
| Retryable | Yes |
| Sample discarded | No |
| Configurable per tenant | Yes |
| HTTP status code | 429 Too Many Requests |
stream_limit
This limit is enforced when a tenant reaches their maximum number of active streams.
Active streams are held in memory buffers in the ingesters, and if this value becomes sufficiently large then it will cause the ingesters to run out of memory.
This value can be modified globally in the limits_config block, or on a per-tenant basis in the runtime overrides file. To increase the allowable active streams, adjust max_global_streams_per_user. Alternatively, the number of active streams can be reduced by removing extraneous labels or removing excessive unique label values.
{{< admonition type="note" >}} Under normal conditions, this limit only blocks the creation of genuinely new streams. Log lines for streams that are already active continue to be accepted.
However, during ingester scaling (autoscaling, rollouts, or ring changes such as a partition-ingester migration), streams can be rebalanced to ingesters that were not previously handling them. The new ingester has no record of that stream in memory, so it treats the next write for that stream as a stream creation and checks it against its local share of the limit. If that share is already used up, for example because the same tenant is also adding new high-cardinality streams during the rebalance, writes to the already-active stream can be rejected with stream_limit, even though the tenant has not added new label cardinality for it.
This effect is temporary. It is bounded by the number of ingesters added or restarted, and it typically resolves once the ring is stable and stream ownership is recalculated. {{< /admonition >}}
| Property | Value |
|---|---|
| Enforced by | ingester |
| Outcome | Request rejected |
| Retryable | Yes |
| Sample discarded | No |
| Configurable per tenant | Yes |
| HTTP status code | 429 Too Many Requests |
Validation Errors
Validation errors occur when a request violates a validation rule defined by Loki.
line_too_long
This error occurs when a log line exceeds the maximum allowable length in bytes. The HTTP response will include the stream to which the offending log line belongs as well as its size in bytes.
This value can be modified globally in the limits_config block, or on a per-tenant basis in the runtime overrides file. To increase the maximum line size, adjust max_line_size. We recommend that you do not increase this value above 256kb for performance reasons. Alternatively, Loki can be configured to ingest truncated versions of log lines over the length limit by using the max_line_size_truncate option. When truncation is enabled, you can use max_line_size_truncate_identifier to append a marker string to each truncated log line, so that truncated lines are easy to identify later.
| Property | Value |
|---|---|
| Enforced by | distributor |
| Outcome | Request rejected |
| Retryable | No |
| Sample discarded | Yes |
| Configurable per tenant | Yes |
| HTTP status code | 400 Bad Request |
invalid_labels
This error occurs when one or more labels in the submitted streams fail validation.
Loki uses the same validation rules as Prometheus for validating labels.
Label names may contain ASCII letters, numbers, as well as underscores. They must match the regex
[a-zA-Z_][a-zA-Z0-9_]*. Label names beginning with __ are reserved for internal use.
| Property | Value |
|---|---|
| Enforced by | distributor |
| Outcome | Request rejected |
| Retryable | No |
| Sample discarded | Yes |
| Configurable per tenant | No |
| HTTP status code | 400 Bad Request |
missing_labels
This validation error is returned when a stream is submitted without any labels.
| Property | Value |
|---|---|
| Enforced by | distributor |
| Outcome | Request rejected |
| Retryable | No |
| Sample discarded | Yes |
| Configurable per tenant | No |
| HTTP status code | 400 Bad Request |
too_far_behind and out_of_order
The too_far_behind validation error is returned when lines in question are older than half of -ingester.max-chunk-age compared to the newest line in the stream.
{{< admonition type="note" >}}
unordered_writes and its related flag -ingester.unordered-writes are deprecated and will be removed in a future major release. Once removed, out-of-order writes will be accepted unconditionally, and this setting will no longer be configurable.
{{< /admonition >}}
More details can be found here about the Loki ordering constraints.
The unordered_writes config value can be modified globally in the limits_config block, or on a per-tenant basis in the runtime overrides file. max_chunk_age, on the other hand, is a global-only configuration and cannot be set per tenant.
This problem can be solved by ensuring that log delivery is configured correctly, or by increasing the max_chunk_age value.
It is recommended to resist modifying the default value of max_chunk_age as this has other implications, and to instead try track down the cause for delayed logged delivery. It should also be noted that this a per-stream error, so by simply splitting streams (adding more labels) this problem can be circumvented, especially if multiple hosts are sending samples for a single stream.
| Property | Value |
|---|---|
| Enforced by | ingester |
| Outcome | Request rejected |
| Retryable | No |
| Sample discarded | Yes |
| Configurable per tenant | Yes, for unordered_writes. No, for max_chunk_age, which is global only. |
| HTTP status code | 400 Bad Request |
greater_than_max_sample_age
If the reject_old_samples config option is set to true (it is by default), then samples will be rejected with reason=greater_than_max_sample_age if they are older than the reject_old_samples_max_age value. You should not see samples rejected for reason=greater_than_max_sample_age if reject_old_samples=false.
This value can be modified globally in the limits_config block, or on a per-tenant basis in the runtime overrides file. This error can be solved by increasing the reject_old_samples_max_age value, or investigating why log delivery is delayed for this particular stream. The stream in question will be returned in the body of the HTTP response.
| Property | Value |
|---|---|
| Enforced by | distributor |
| Outcome | Request rejected |
| Retryable | No |
| Sample discarded | Yes |
| Configurable per tenant | Yes |
| HTTP status code | 400 Bad Request |
too_far_in_future
If a sample's timestamp is greater than the current timestamp, Loki allows for a certain grace period during which samples will be accepted. If the grace period is exceeded, the error will occur.
This value can be modified globally in the limits_config block, or on a per-tenant basis in the runtime overrides file. This error can be solved by increasing the creation_grace_period value, or investigating why this particular stream has a timestamp too far into the future. The stream in question will be returned in the body of the HTTP response.
| Property | Value |
|---|---|
| Enforced by | distributor |
| Outcome | Request rejected |
| Retryable | No |
| Sample discarded | Yes |
| Configurable per tenant | Yes |
| HTTP status code | 400 Bad Request |
max_label_names_per_series
If a sample is submitted with more labels than Loki has been configured to allow, it will be rejected with the max_label_names_per_series reason. Note that 'series' is the same thing as a 'stream' in Loki - the 'series' term is a legacy name.
This value can be modified globally in the limits_config block, or on a per-tenant basis in the runtime overrides file. This error can be solved by increasing the max_label_names_per_series value. The stream to which the offending sample (i.e. the one with too many label names) belongs will be returned in the body of the HTTP response.
| Property | Value |
|---|---|
| Enforced by | distributor |
| Outcome | Request rejected |
| Retryable | No |
| Sample discarded | Yes |
| Configurable per tenant | Yes |
| HTTP status code | 400 Bad Request |
label_name_too_long
If a sample is sent with a label name that has a length in bytes greater than Loki has been configured to allow, it will be rejected with the label_name_too_long reason.
This value can be modified globally in the limits_config block, or on a per-tenant basis in the runtime overrides file. This error can be solved by increasing the max_label_name_length value, though we do not recommend raising it significantly above the default value of 1024 for performance reasons. The offending stream will be returned in the body of the HTTP response.
| Property | Value |
|---|---|
| Enforced by | distributor |
| Outcome | Request rejected |
| Retryable | No |
| Sample discarded | Yes |
| Configurable per tenant | Yes |
| HTTP status code | 400 Bad Request |
label_value_too_long
If a sample has a label value with a length in bytes greater than Loki has been configured to allow, it will be rejected for the label_value_too_long reason.
This value can be modified globally in the limits_config block, or on a per-tenant basis in the runtime overrides file. This error can be solved by increasing the max_label_value_length value. The offending stream will be returned in the body of the HTTP response.
| Property | Value |
|---|---|
| Enforced by | distributor |
| Outcome | Request rejected |
| Retryable | No |
| Sample discarded | Yes |
| Configurable per tenant | Yes |
| HTTP status code | 400 Bad Request |
duplicate_label_names
If a sample is sent with two or more identical labels, it will be rejected for the duplicate_label_names reason.
The offending stream will be returned in the body of the HTTP response.
| Property | Value |
|---|---|
| Enforced by | distributor |
| Outcome | Request rejected |
| Retryable | No |
| Sample discarded | Yes |
| Configurable per tenant | No |
| HTTP status code | 400 Bad Request |
request_body_too_large
This error occurs when the raw push request exceeds a size limit before Loki can parse individual streams and log lines out of it. Unlike the other validation errors on this page, request_body_too_large can be triggered in two different ways:
- The decompressed request body exceeds the
max_recv_msg_sizeormax_decompressed_sizesettings in thedistributorconfiguration block. These are cluster-wide settings, notlimits_configvalues, so they cannot be set per tenant.max_recv_msg_sizedefaults to 100MB, andmax_decompressed_sizedefaults to 5000MB (50 times the defaultmax_recv_msg_size). - A single stream's label string exceeds 16MB. This limit is hardcoded and cannot be changed through any configuration option. It exists because the Prometheus label parser cannot handle labels above this size.
To resolve this error, reduce the size of the push request (for example by sending fewer streams per request, or by reducing the number or size of labels on a stream), or increase max_recv_msg_size and max_decompressed_size if your cluster has the resources to handle larger requests.
{{< admonition type="note" >}}
This reason only appears in the loki_discarded_bytes_total metric, not in loki_discarded_samples_total. Loki rejects the request before it can parse the body, so it cannot count how many log lines the request contained.
{{< /admonition >}}
| Property | Value |
|---|---|
| Enforced by | distributor |
| Outcome | Request rejected |
| Retryable | No |
| Sample discarded | Yes |
| Configurable per tenant | No |
| HTTP status code | 413 Request Entity Too Large |
disallowed_structured_metadata
This error occurs when a stream includes structured metadata but the tenant is not allowed to send it.
This value can be modified globally in the limits_config block, or on a per-tenant basis in the runtime overrides file. To allow structured metadata, set allow_structured_metadata to true (this is the default). The offending stream will be returned in the body of the HTTP response.
| Property | Value |
|---|---|
| Enforced by | distributor |
| Outcome | Request rejected |
| Retryable | No |
| Sample discarded | Yes |
| Configurable per tenant | Yes |
| HTTP status code | 400 Bad Request |
structured_metadata_too_large
This error occurs when the structured metadata attached to a single log line exceeds the maximum allowable size in bytes.
This value can be modified globally in the limits_config block, or on a per-tenant basis in the runtime overrides file. To increase the limit, adjust max_structured_metadata_size (default: 64kb). The offending stream will be returned in the body of the HTTP response.
| Property | Value |
|---|---|
| Enforced by | distributor |
| Outcome | Request rejected |
| Retryable | No |
| Sample discarded | Yes |
| Configurable per tenant | Yes |
| HTTP status code | 400 Bad Request |
structured_metadata_too_many
This error occurs when a single log line includes more structured metadata entries than Loki has been configured to allow.
This value can be modified globally in the limits_config block, or on a per-tenant basis in the runtime overrides file. To increase the limit, adjust max_structured_metadata_entries_count (default: 128). The offending stream will be returned in the body of the HTTP response.
| Property | Value |
|---|---|
| Enforced by | distributor |
| Outcome | Request rejected |
| Retryable | No |
| Sample discarded | Yes |
| Configurable per tenant | Yes |
| HTTP status code | 400 Bad Request |
blocked_ingestion and blocked_ingestion_policy
{{< admonition type="note" >}} This is an experimental feature. {{< /admonition >}}
These reasons occur when an operator has deliberately blocked ingestion for a tenant (blocked_ingestion) or for a specific policy (blocked_ingestion_policy), rather than because a client sent invalid or excessive data. Policies are defined using the policy_stream_mapping setting in the limits_config block.
This is configured using block_ingestion_until (for a tenant) or block_ingestion_policy_until (for a policy), both of which take a date in RFC3339 format. Ingestion is blocked until that date. These values can be set globally in the limits_config block, but they are normally set for a single tenant in the runtime overrides file, because a global value blocks ingestion for every tenant.
The block_ingestion_status_code option controls what HTTP status code Loki returns while ingestion is blocked. The default is a custom status code, 260, so that blocked requests can be distinguished from ordinary errors. If block_ingestion_status_code is set to 200, Loki silently drops the blocked data without returning an error to the client, although the data is still counted in loki_discarded_samples_total and loki_discarded_bytes_total.
| Property | Value |
|---|---|
| Enforced by | distributor |
| Outcome | Request rejected, or silently dropped if block_ingestion_status_code is 200 |
| Retryable | No |
| Sample discarded | Yes |
| Configurable per tenant | Yes |
| HTTP status code | 260 by default, configurable with block_ingestion_status_code |
missing_enforced_labels
{{< admonition type="note" >}} This is an experimental feature. {{< /admonition >}}
This error occurs when a tenant has been configured to require certain labels on every stream, and a stream is submitted that is missing one or more of them.
This value can be modified globally in the limits_config block, or on a per-tenant basis in the runtime overrides file. Use enforced_labels to require labels on all streams, or policy_enforced_labels to require them only on streams matching a specific policy defined by policy_stream_mapping. The stream and the missing labels will be returned in the body of the HTTP response.
| Property | Value |
|---|---|
| Enforced by | distributor |
| Outcome | Request rejected |
| Retryable | No |
| Sample discarded | Yes |
| Configurable per tenant | Yes |
| HTTP status code | 400 Bad Request |
Manage larger production deployments
When needing to scale Loki due to increased log volume, operators should consider running several Loki processes
partitioned by role (ingester, distributor, querier, and so on) rather than a single Loki
process. Grafana Labs' production setup
contains .libsonnet files that demonstrates configuring separate components
and scaling for resource usage.
Separate Query Scheduler
The Query frontend has an in-memory queue that can be moved out into a separate process similar to the Grafana Mimir query-scheduler. This allows running multiple query frontends.
To run with the Query Scheduler, configure both the frontend and the querier worker with the scheduler's address.
Using CLI flags:
- Frontend:
-frontend.scheduler-address=<scheduler-host:port> - Querier:
-querier.scheduler-address=<scheduler-host:port>
Using the configuration file:
frontend:
scheduler_address: <scheduler-host:port>
frontend_worker:
scheduler_address: <scheduler-host:port>
{{< admonition type="note" >}}
The querier's scheduler_address is configured under frontend_worker, not under querier. The frontend_worker block configures the worker running within the querier that pulls and executes queries.
{{< /admonition >}}
It is not valid to start the querier with both a configured frontend address and a scheduler address.
The query scheduler process itself can be started via the -target=query-scheduler option of the Loki Docker image. For instance, docker run grafana/loki:latest -config.file=/etc/loki/config.yaml -target=query-scheduler -server.http-listen-port=8009 -server.grpc-listen-port=9009 starts the query scheduler listening on ports 8009 and 9009.
Scheduler discovery using a ring
Instead of configuring a static scheduler_address on the frontend and the querier worker, you can have query schedulers register themselves in a hash ring. Queriers and query frontends then discover query schedulers through the ring instead of using a fixed address.
To enable this, set use_scheduler_ring: true, or use the -query-scheduler.use-scheduler-ring CLI flag:
query_scheduler:
use_scheduler_ring: true
{{< admonition type="note" >}}
Set this option in the configuration used by your query schedulers, your queriers, and your query frontends. Queriers and query frontends only read the ring if use_scheduler_ring is true in their own configuration. If all of your components share one configuration file, you only need to set it once.
{{< /admonition >}}
The ring needs a key-value store. In most deployments you do not need to configure one for the query scheduler specifically, because the scheduler ring inherits the store from the common.ring block, and Loki uses memberlist for all rings when you configure a memberlist section. To set the store for the scheduler ring alone, use the scheduler_ring block:
query_scheduler:
use_scheduler_ring: true
scheduler_ring:
kvstore:
store: memberlist
If you do not configure a frontend address, a scheduler address, or a downstream URL anywhere in your configuration, Loki automatically enables the scheduler ring for you.
Each component that takes part in the ring exposes the ring state at the /scheduler/ring endpoint, which you can use to check that all query schedulers registered as expected.
Memory ballast
In compute-constrained environments, garbage collection can become a significant performance factor. Frequently-run garbage collection interferes with running the application by using CPU resources. The use of memory ballast can mitigate the issue. Memory ballast allocates extra, but unused virtual memory in order to inflate the quantity of live heap space. Garbage collection is triggered by the growth of heap space usage. The inflated quantity of heap space reduces the perceived growth, so garbage collection occurs less frequently.
Configure memory ballast using the ballast_bytes configuration option.
Remote rule evaluation
This feature was first proposed in LID-0002; it contains the design decisions
which informed the implementation.
By default, the ruler component embeds a query engine to evaluate rules. This generally works fine, except when rules
are complex or have to process a large amount of data regularly. Poor performance of the ruler manifests as recording rules metrics
with gaps or missed alerts. This situation can be detected by alerting on the loki_prometheus_rule_group_iterations_missed_total metric
when it has a non-zero value.
A solution to this problem is to externalize rule evaluation from the ruler process. The ruler embedded query engine
is single-threaded, meaning that rules are not split, sharded, or otherwise accelerated like regular Loki queries. The query-frontend
component exists explicitly for this purpose and, when combined with a number of querier instances, can massively
improve rule evaluation performance and lead to fewer missed iterations.
It is generally recommended to create a separate query-frontend deployment and querier pool from your existing one - which handles adhoc
queries via Grafana, logcli, or the API. Rules should be given priority over adhoc queries because they are used to produce
metrics or alerts which may be crucial to the reliable operation of your service; if you use the same query-frontend and querier pool
for both, your rules will be executed with the same priority as adhoc queries which could lead to unpredictable performance.
To enable remote rule evaluation, set the following configuration options:
ruler:
evaluation:
mode: remote
query_frontend:
address: dns:///<query-frontend-service>:<grpc-port>
If the query-frontend connection requires TLS, set tls_enabled: true under query_frontend and configure the accompanying TLS options.
To reduce contention when many rules evaluate at the same time, set max_jitter to add a bounded, random delay before each rule evaluation. This option applies to both local and remote evaluation modes:
ruler:
evaluation:
max_jitter: 5s
Refer to the ruler configuration reference for further configuration options.
When you enable remote rule evaluation, the ruler component becomes a gRPC client to the query-frontend service;
this will result in far lower ruler resource usage because the majority of the work has been externalized.
The LogQL queries coming from the ruler will be executed against the given query-frontend service.
Requests will be load-balanced across all query-frontend IPs if the dns:/// prefix is used.
{{< admonition type="note" >}} Queries that fail to execute are not retried. {{< /admonition >}}
Limits and Observability
Remote rule evaluation can be tuned with the following options:
ruler_remote_evaluation_timeout: maximum allowable execution time for rule evaluationsruler_remote_evaluation_max_response_size: maximum allowable response size over gRPC connection fromquery-frontendtoruler
Both of these can be specified globally in the limits_config section
or on a per-tenant basis.
max_jitter, described earlier in this topic, is a global-only setting under ruler.evaluation rather than a per-tenant limit.
Remote rule evaluation exposes a number of metrics:
loki_ruler_remote_eval_request_duration_seconds: time taken for rule evaluation (histogram)loki_ruler_remote_eval_response_bytes: number of bytes in rule evaluation response (histogram)loki_ruler_remote_eval_response_samples: number of samples in rule evaluation response (histogram)loki_ruler_remote_eval_success_total: successful rule evaluations (counter)loki_ruler_remote_eval_failure_total: unsuccessful rule evaluations with reasons (counter)
Each of these metrics are per-tenant, so cardinality must be taken into consideration.
Manage version upgrades
-
Upgrade from one Loki version to a newer version.
-
Upgrade Helm from Helm v2.x to Helm v3.x.
Speed up ingester rollout using zone awareness
The Loki zone aware ingesters are used by Grafana Labs in order to allow for easier rollouts of large Loki deployments. You can think of them as three logical zones, however with some extra Kubernetes configuration you could deploy them in separate zones.
By default, an incoming log stream's logs are replicated to 3 random ingesters. Except in the case of some replica scaling up or down, a given stream will always be replicated to the same 3 ingesters. This means that if one of those ingesters is restarted no data is lost. However two or more ingesters restarting can result in data loss and also impacts the systems ability to ingest logs because of an unhealthy ring status.
With zone awareness enabled, an incoming log line will be replicated to one ingester in each zone. This means that we're not only concerned about ingesters in multiple zones restarting at the same time, we can now rollout or lose an entire zone at once without impacting writes. This allows deployments with a large number of ingesters to be deployed much more quickly.
At Grafana Labs, we also make use of rollout-operator to manage rollouts to the 3 StatefulSets gracefully. The rollout-operator looks for labels on StatefulSets to know which ones are part of a certain rollout group, and coordinates rollouts of pods only from a single StatefulSet in the group at a time. See the README in the rollout-operator repo for a more in depth explanation.
{{< admonition type="note" >}} This guide describes the jsonnet deployment method used internally at Grafana Labs. New jsonnet deployments run zone aware ingesters by default, so this guide is only needed if you have an existing deployment that still uses a single ingester StatefulSet.
If you deploy Loki with the Helm chart instead, refer to Zone-aware replication in the Helm chart components documentation, which uses ingester.zoneAwareReplication Helm values rather than the jsonnet configuration described here.
The number of zones is fixed at 3 in the jsonnet library described in this guide. There is no configuration option to run a different number of zones. {{< /admonition >}}
Migration
Migrating from a single ingester StatefulSet to 3 zone aware ingester StatefulSets. The migration follows a few general steps, regardless of deployment method.
-
Configure your existing ingesters to be part of a zone, for example
zone-default, this will allow us to later exclude them from the write path while still allowing for graceful shutdowns. -
Prep for the increase in active streams (due to the way streams are split between ingesters) by increasing the number of active streams allowed for your tenants.
-
Add and scale up your new zone-aware ingester StatefulSets such that each has 1/3rd of the total number of replicas you want to run.
-
Enable zone awareness on the write path by setting
distributor.zone-awareness-enabledtotruefor distributors and rulers. -
Wait some time to ensure that the new zone-aware ingesters have data for the time period they are queried for (
query_ingesters_within). -
Enable zone awareness on the read path by setting
distributor.zone-awareness-enabledto true for queriers.{{< admonition type="note" >}}
distributor.zone-awareness-enabledanddistributor.excluded-zonesare CLI flag names. Despite thedistributor.prefix, they configure the ingester ring, so each process that uses that ring (distributor, ruler, and querier) reads the same flag. In YAML, the equivalent keys arezone_awareness_enabledandexcluded_zonesunderingester.lifecycler.ring, not under a top-leveldistributor:block. {{< /admonition >}} -
Configure distributors and rulers to exclude ingesters in the
zone-defaultso those ingesters no longer receive write traffic usingdistributor.excluded-zones. -
Use the shutdown endpoint to flush data from the default ingesters, then scale down and remove the associated StatefulSet.
-
Clean up any config remaining from the migration.
Detailed Migration Steps
The following are steps to live migrate (no downtime) an existing Loki deployment from a single ingester StatefulSet to 3 zone aware ingester StatefulSets.
These instructions assume you are using the zone aware ingester jsonnet deployment code from this repo, see here. If you are not using jsonnet see the relevant annotations in some steps that describe how to perform that step manually.
-
Configure the zone for the existing ingester StatefulSet as
zone-defaultby settingmulti_zone_default_ingester_zone: true, this allows us to later filter out that zone from the write path.If you're not using jsonnet, this is done by setting the
-ingester.availability-zone=zone-defaultflag (or the equivalentavailability_zonefield under the ingester'slifecyclerYAML configuration) on the existing ingesters. -
Configure ingester-pdb with
maxUnavailableas 0 and deploy 3x zone-aware StatefulSets with 0 replicas by setting_config+:: { multi_zone_ingester_enabled: true, multi_zone_ingester_migration_enabled: true, multi_zone_ingester_replicas: 0, // These last two lines are necessary now that we enable zone aware ingester by default // so that newly created cells will not be migrated later on. If you miss them you will // break writes in the cell. multi_zone_ingester_replication_write_path_enabled: false, multi_zone_ingester_replication_read_path_enabled: false, },Setting
multi_zone_ingester_migration_enabled: truealready configures the existingingesterPodDisruptionBudget (PDB) withmaxUnavailable: 0for you, so no manual PDB change is needed when using jsonnet.If you're not using jsonnet, manually set
maxUnavailableto0on the existing ingester-pdb. The new ingester StatefulSets should have a label withrollout-group: ingester, annotationrollout-max-unavailable: x(put a placeholder value in, later you should set the value of this to be some portion of the StatefulSets total replicas, for example in jsonnet we template this so that each StatefulSet runs 1/3 of the total replicas and the max unavailable is 1/3 of each StatefulSets replicas), set the update strategy toOnDelete, and set-ingester.availability-zone=zone-<a|b|c>for each of the three new StatefulSets. -
Diff ingester and ingester-zone-a StatefulSets and make sure all config matches
kubectl get statefulset -n loki-dev-008 ingester -o yaml > ingester.yaml kubectl get statefulset -n loki-dev-008 ingester-zone-a -o yaml > ingester-zone-a.yaml diff ingester.yaml ingester-zone-a.yamlExpected in diffs are values like: creation time and revision number, the zone, fields used by rollout operator, number of replicas, anything related to kustomize/flux, and PVC for the WAL since the containers don't exist yet.
-
Temporarily double max series limits for users that are using more than 50% of their current limit, the queries are as follows (add label selectors as appropriate):
sum by (tenant)(sum (loki_ingester_memory_streams) by (cluster, namespace, tenant) / on (namespace) group_left max by(namespace) (loki_distributor_replication_factor)) > on (tenant) ( max by (tenant) (label_replace(loki_overrides{limit_name="max_global_streams_per_user"} / 2.5, "tenant", "$1", "user", "(.+)")) )(sum (loki_ingester_memory_streams) by (cluster, namespace, tenant) / on (namespace) group_left max by(namespace) (loki_distributor_replication_factor) ) / ignoring(tenant) group_left max by (cluster, namespace)(loki_overrides_defaults{limit_name="max_global_streams_per_user"}) > 0.4) unless on (tenant) ( (label_replace(loki_overrides{limit_name="max_global_streams_per_user"},"tenant", "$1", "user", "(.+)"))) -
Scale up zone-aware StatefulSets until they have 1/3rd of replicas each. In smaller cells you can do this all at once, in larger cells it is safer to do it in chunks. The config value you need to change is
multi_zone_ingester_replicas: 6, the value will be split across the three StatefulSets. In this case, each StatefulSet would run 2 replicas.If you're not using jsonnet, this is the step where you would also set the annotation
rollout-max-unavailableto some value that is less than or equal to the number of replicas each StatefulSet is running.By default, each zone's StatefulSet uses pod anti-affinity so that ingesters from two different zones are never scheduled on the same node. Two ingesters from the same zone are still allowed to share a node. This anti-affinity rule is controlled by
ingester_allow_multiple_replicas_on_same_node, which defaults tofalse.To also spread the ingesters within a single zone evenly across nodes, set
multi_zone_ingester_use_topology_spread: true. This adds a topology spread constraint to each zone's StatefulSet, which you can tune withmulti_zone_ingester_topology_spread_max_skewandmulti_zone_ingester_topology_spread_when_unsatisfiable. -
Enable zone awareness on the write path by setting
multi_zone_ingester_replication_write_path_enabled: true, this causes distributors and rulers to reshuffle series to distributors in each zone. Be sure to check that all the distributors and rulers have restarted properly.If you're not using jsonnet, enable zone awareness on the write path by setting
distributor.zone-awareness-enabledto true for distributors and rulers. -
Wait for
query_ingesters_withinconfigured hours. The default is3h. This ensures that no data will be missing if we query a new ingester. However, because we cut chunks at least every 30m due tochunk_idle_periodwe can likely reduce this amount of time. -
Check that rule evaluations are still correct on the migration, look for increases in the rate for metrics with names with the following suffixes:
rule_evaluations_total rule_evaluation_failures_total rule_group_iterations_missed_total -
Enable zone-aware replication on the read path
multi_zone_ingester_replication_read_path_enabled: true. If you're not using jsonnet, setdistributor.zone-awareness-enabledto true for queriers. -
Check that queries are still executing correctly, for example look at
loki_logql_querystats_latency_seconds_countto see that you don't have a big increase in latency or error count for a specific query type. -
Configure distributor / ruler to exclude ingesters in the
zone-defaultso those ingesters no longer receive write traffic by settingmulti_zone_ingester_exclude_default: true. If you're not using jsonnet setdistributor.excluded-zoneson distributors and rulers.It is a good idea to check rules evaluations again at this point, and also that the zone aware ingester StatefulSet is now receiving all the write traffic, you can compare
sum(loki_ingester_memory_streams{cluster="<cluster>",job=~"(<namespace>)/ingester"})tosum(loki_ingester_memory_streams{cluster="<cluster>",job=~"(<namespace>)/ingester-zone.*"}) -
If you're using an automated reconciliation or deployment system like flux, disable it now (for example using flux ignore) if possible for just the default ingester StatefulSet.
-
Shutdown flush the default ingesters, unregistering them from the ring, you can do this by port-forwarding each ingester Pod and using the endpoint:
"http://url:PORT/ingester/shutdown?flush=true&delete_ring_tokens=true&terminate=false"See Flush in-memory chunks and shut down for details on this endpoint. For Kubernetes-native scale-down automation, you can instead use
POST /ingester/prepare_shutdown, which arms the ingester to flush and unregister on its nextSIGTERMrather than shutting it down immediately. -
manually scale down the default ingester StatefulSet to 0 replicas, we do this via
tk applybut you could do it via modifying the yaml. -
merge a PR to your central config repo to keep the StatefulSet 0'd, and then remove the flux ignore.
-
clean up any remaining temporary config from the migration, for example
multi_zone_ingester_migration_enabled: trueis no longer needed. -
ensure that all the old default ingester PVC/PV are removed.
Deploy Loki meta-monitoring with chart version 3
The primary method for collecting and monitoring a Loki cluster is to use the Kubernetes Monitoring Helm chart. This chart provides a comprehensive monitoring solution for Kubernetes clusters and includes direct integrations for monitoring the full LGTM (Loki, Grafana, Tempo, and Mimir) stack. This procedure will walk you through deploying version 3.8.x of the Kubernetes Monitoring Helm chart to monitor your Loki cluster.
{{< admonition type="note" >}} This page covers chart version 3.8.x, which is a maintained stable release line. If you do not already have a reason to stay on chart 3.8.x, use Deploy Loki meta-monitoring instead, which covers the current chart 4.x line. For help moving from chart 3.x to 4.x, refer to the Kubernetes Monitoring Helm chart migration guide. {{< /admonition >}}
{{< admonition type="note" >}} We recommend running a production cluster of Loki in distributed mode using Kubernetes. This procedure assumes you have a running Kubernetes cluster and a running Loki deployment. There are other methods for deploying Loki, such as using Docker or VM installations. meta-monitoring is still possible when using these deployment methods but not covered in this procedure. {{< /admonition >}}
Prerequisites
- kubectl
- Helm 3 or above. See Installing Helm.
- A running Kubernetes cluster with a running Loki deployment.
- A Grafana Cloud account or a separate LGTM stack for monitoring.
Preparing your environment
Before deploying the Kubernetes Monitoring Helm chart, you need to set up several components in your environment.
-
Add the Grafana Helm repository:
helm repo add grafana https://grafana.github.io/helm-charts -
Update your Helm repositories:
helm repo update -
Create a namespace for the monitoring stack:
kubectl create namespace meta
Authentication
The Kubernetes Monitoring Helm chart requires a Grafana Cloud account or a separate LGTM stack for monitoring. You will need to provide the necessary credentials to the Helm chart to authenticate with your Grafana Cloud account or LGTM stack. In this procedure, we will use Grafana Cloud as an example.
-
Create a new Cloud Access Policy in Grafana Cloud.
- Sign into Grafana Cloud.
- In the main menu, select Security > Access Policies.
- Click Create access policy.
- Give the policy a Name and select the following permissions:
- Metrics: Write
- Logs: Write
- Click Create.
- Click Add Token. Give the token a name and click Create. Save the token for later use.
-
Collect
urlandusernamefor Prometheus and Loki.- Navigate to the Grafana Cloud Portal Overview page.
- Click the Details button for your Prometheus instance.
- From the Sending metrics using Grafana Alloy section, collect the instance username and url.
- Navigate back to the Overview page.
- Click the Details button for your Loki instance.
- From the Sending Logs to Grafana Cloud using Grafana Alloy section, collect the instance username and url.
- Navigate back to the Overview page.
-
Create the Kubernetes Secrets with the collected credentials from Grafana Cloud.
kubectl create secret generic metrics -n meta \ --from-literal=username=<PROMETHEUS-USER> \ --from-literal=password=<CLOUD-TOKEN> kubectl create secret generic logs -n meta \ --from-literal=username=<LOKI-USER> \ --from-literal=password=<CLOUD-TOKEN>
Note that the Kubernetes Monitoring Helm supports many different authentication methods based upon your requirements including:
- Bearer Tokens
- OAuth2
- SigV4
- External Secrets
For further information on how to configure these methods, see the Kubernetes Monitoring Helm examples.
Deploy the Kubernetes Monitoring Helm chart
Now that you have prepared your environment and collected the necessary credentials, you can deploy the Kubernetes Monitoring Helm chart to monitor your Loki cluster. To do this we need to copy a values.yaml file to our local machine and modify it to include the necessary configuration.
-
Download the
values-chart-v3.yamlfile from the Kubernetes Monitoring Helm chart repository:curl -O https://raw.githubusercontent.com/grafana/loki/main/production/helm/meta-monitoring/values-chart-v3.yaml -
Open the
values-chart-v3.yamlfile in a text editor of your choosing and add the Prometheus and Loki endpoints.destinations: - name: prometheus type: prometheus url: https://<PROMETHEUS-ENDPOINT>/api/prom/push auth: type: basic usernameKey: username passwordKey: password secret: create: false name: metrics namespace: meta - name: loki type: loki url: https://<LOKI-ENDPOINT>/loki/api/v1/push auth: type: basic usernameKey: username passwordKey: password secret: create: false name: logs namespace: meta -
(Optional) Update the cluster name to a human-readable name to identify your cluster in Grafana Cloud.
# Global Label to be added to all telemetry data. Should reflect a recognizable name for the cluster. cluster: name: loki-meta-monitoring-cluster -
The default values file assumes that you have deployed Loki in the
lokinamespace and will deploy the Kubernetes monitoring stack in themetanamespace. If you have deployed Loki in a different namespace, or the monitoring stack in a namespace other thanmeta, you will need to update several keys invalues-chart-v3.yaml. There is no singlenamespaceskey; namespace scoping is set independently for each feature:Key Format Purpose integrations.loki.instances[0].namespaceslist Namespace(s) to discover Loki instances in. integrations.alloy.instances[0].namespaceslist Namespace(s) to discover the monitoring stack's own Alloy collectors in. clusterEvents.namespaceslist Namespace(s) to capture Kubernetes events from. clusterMetrics.cadvisor.metricsTuning.includeNamespaceslist Namespace(s) to keep cadvisor metrics for. This is a metric filter, not a discovery scope. clusterMetrics.kube-state-metrics.namespaceslist, or comma-separated string Namespace(s) to collect Kubernetes resource state from. podLogs.namespaceslist Namespace(s) to collect pod logs from. For example, to add a namespace to the Loki discovery scope:
integrations: loki: instances: - name: loki namespaces: - lokiEach of these keys defaults to an empty list, which means "collect from all namespaces". Removing a key restores that default for that feature only; it does not affect the other keys in this table.
Also note that
destinations[].secret.namespace(used for both themetricsandlogsdestinations) must match the namespace where you created themetricsandlogssecrets, which ismetaby default. Update it if you installed the monitoring stack into a different namespace. -
Deploy the Kubernetes Monitoring Helm chart using the modified
values-chart-v3.yamlfile, pinning the chart to the 3.x line:helm install meta-loki grafana/k8s-monitoring \ --namespace meta \ --version "^3" \ -f values-chart-v3.yaml -
Verify that the Kubernetes Monitoring Helm chart has been deployed successfully:
kubectl get pods -n metaYou should see a list of pods running in the
metanamespace.NAME READY STATUS RESTARTS ... meta-loki-alloy-singleton-6d7f8d8b86-sg4wx 2/2 Running 0 ... meta-loki-kube-state-metrics-64bdcfcbd-5snqz 1/1 Running 0 ... meta-loki-node-exporter-855l5 1/1 Running 0 ... meta-loki-node-exporter-b976b 1/1 Running 0 ... meta-loki-node-exporter-vsm4s 1/1 Running 0 ...
Next Steps
You have successfully deployed the Kubernetes Monitoring Helm chart to monitor your Loki cluster. You can now move onto the next step of deploying the Loki mixin to visualize the metrics and logs from your Loki cluster. For more information, see Install the Loki Mixin.
Deploy Loki meta-monitoring
The primary method for collecting and monitoring a Loki cluster is to use the Kubernetes Monitoring Helm chart. This chart provides a comprehensive monitoring solution for Kubernetes clusters and includes direct integrations for monitoring the full LGTM (Loki, Grafana, Tempo, and Mimir) stack. This procedure will walk you through deploying the Kubernetes Monitoring Helm chart to monitor your Loki cluster.
{{< admonition type="note" >}} We recommend running a production cluster of Loki in distributed mode using Kubernetes. This procedure assumes you have a running Kubernetes cluster and a running Loki deployment. There are other methods for deploying Loki, such as using Docker or VM installations. meta-monitoring is still possible when using these deployment methods but not covered in this procedure. If you run Loki in monolithic mode outside of this Helm chart, refer to Single binary meta-monitoring instead. {{< /admonition >}}
{{< admonition type="note" >}} This page covers chart version 4.x, the current line. If you need to stay on the maintained 3.8.x stable line instead, use Deploy Loki meta-monitoring with chart version 3. {{< /admonition >}}
Prerequisites
- kubectl
- Helm 3 or above. See Installing Helm.
- A running Kubernetes cluster with a running Loki deployment.
- A Grafana Cloud account or a separate LGTM stack for monitoring.
Preparing your environment
Before deploying the Kubernetes Monitoring Helm chart, you need to set up several components in your environment.
-
Add the Grafana Helm repository:
helm repo add grafana https://grafana.github.io/helm-charts -
Update your Helm repositories:
helm repo update -
Create a namespace for the monitoring stack:
kubectl create namespace meta
Authentication
The Kubernetes Monitoring Helm chart requires a Grafana Cloud account or a separate LGTM stack for monitoring. You will need to provide the necessary credentials to the Helm chart to authenticate with your Grafana Cloud account or LGTM stack. In this procedure, we will use Grafana Cloud as an example.
-
Create a new Cloud Access Policy in Grafana Cloud.
- Sign into Grafana Cloud.
- In the main menu, select Security > Access Policies.
- Click Create access policy.
- Give the policy a Name and select the following permissions:
- Metrics: Write
- Logs: Write
- Click Create.
- Click Add Token. Give the token a name and click Create. Save the token for later use.
-
Collect
urlandusernamefor Prometheus and Loki.- Navigate to the Grafana Cloud Portal Overview page.
- Click the Details button for your Prometheus instance.
- From the Sending metrics using Grafana Alloy section, collect the instance username and url.
- Navigate back to the Overview page.
- Click the Details button for your Loki instance.
- From the Sending Logs to Grafana Cloud using Grafana Alloy section, collect the instance username and url.
- Navigate back to the Overview page.
-
Create the Kubernetes Secrets with the collected credentials from Grafana Cloud.
kubectl create secret generic metrics -n meta \ --from-literal=username=<PROMETHEUS-USER> \ --from-literal=password=<CLOUD-TOKEN> kubectl create secret generic logs -n meta \ --from-literal=username=<LOKI-USER> \ --from-literal=password=<CLOUD-TOKEN>
Note that the Kubernetes Monitoring Helm supports many different authentication methods based upon your requirements including:
- Bearer Tokens
- OAuth2
- SigV4
- External Secrets
For further information on how to configure these methods, see the Kubernetes Monitoring Helm examples.
Deploy the Kubernetes Monitoring Helm chart
Now that you have prepared your environment and collected the necessary credentials, you can deploy the Kubernetes Monitoring Helm chart to monitor your Loki cluster. To do this we need to copy a values.yaml file to our local machine and modify it to include the necessary configuration.
-
Download the
values.yamlfile from the Kubernetes Monitoring Helm chart repository:curl -O https://raw.githubusercontent.com/grafana/loki/main/production/helm/meta-monitoring/values.yaml -
Open the
values.yamlfile in a text editor of your choosing and add the Prometheus and Loki endpoints. Chart 4.x configures destinations as a map keyed by name, rather than a list:destinations: prometheus: type: prometheus url: https://<PROMETHEUS-ENDPOINT>/api/prom/push auth: type: basic usernameKey: username passwordKey: password secret: create: false name: metrics namespace: meta loki: type: loki url: https://<LOKI-ENDPOINT>/loki/api/v1/push auth: type: basic usernameKey: username passwordKey: password secret: create: false name: logs namespace: meta -
(Optional) Update the cluster name to a human-readable name to identify your cluster in Grafana Cloud.
# Global Label to be added to all telemetry data. Should reflect a recognizable name for the cluster. cluster: name: loki-meta-monitoring-cluster -
Confirm that at least one collector is defined under
collectors:. Chart 4.x requires this; the install fails ifcollectors:is empty or missing. The defaultvalues.yamlalready defines thealloy-singletoncollector that every feature in this file is assigned to:collectors: alloy-singleton: presets: [singleton]If you add or rename a collector, update every
collector: alloy-singletonreference in the file to match, or the install fails with an error naming the missing collector. -
The default values file assumes that you have deployed Loki in the
lokinamespace and will deploy the Kubernetes monitoring stack in themetanamespace. If you have deployed Loki in a different namespace, or the monitoring stack in a namespace other thanmeta, you will need to update several keys invalues.yaml. There is no singlenamespaceskey; namespace scoping is set independently for each feature:Key Format Purpose integrations.loki.instances[0].namespaceslist Namespace(s) to discover Loki instances in. integrations.alloy.instances[0].namespaceslist Namespace(s) to discover the monitoring stack's own Alloy collectors in. clusterEvents.namespaceslist Namespace(s) to capture Kubernetes events from. clusterMetrics.cadvisor.metricsTuning.includeNamespaceslist Namespace(s) to keep cadvisor metrics for. This is a metric filter, not a discovery scope. telemetryServices.kube-state-metrics.namespaceslist, or comma-separated string Namespace(s) to collect Kubernetes resource state from. In chart 4.x this key must be set under telemetryServices, not underclusterMetrics.podLogsViaKubernetesApi.namespaceslist Namespace(s) to collect pod logs from. For example, to add a namespace to the Loki discovery scope:
integrations: loki: instances: - name: loki namespaces: - lokiEach of these keys defaults to an empty list, which means "collect from all namespaces". Removing a key restores that default for that feature only; it does not affect the other keys in this table.
Also note that
destinations.prometheus.secret.namespaceanddestinations.loki.secret.namespacemust match the namespace where you created themetricsandlogssecrets, which ismetaby default. Update them if you installed the monitoring stack into a different namespace. -
Deploy the Kubernetes Monitoring Helm chart using the modified
values.yamlfile, pinning the chart to the 4.x line:helm install meta-loki grafana/k8s-monitoring \ --namespace meta \ --version "^4" \ -f values.yaml -
Verify that the Kubernetes Monitoring Helm chart has been deployed successfully:
kubectl get pods -n metaYou should see a list of pods running in the
metanamespace, similar to the following. Exact pod names and counts depend on your cluster size and which features you have enabled:NAME READY STATUS RESTARTS ... meta-loki-alloy-operator-7d8f9c6b5d-x2k9p 1/1 Running 0 ... meta-loki-alloy-singleton-6d7f8d8b86-sg4wx 2/2 Running 0 ... meta-loki-kube-state-metrics-64bdcfcbd-5snqz 1/1 Running 0 ... meta-loki-node-exporter-855l5 1/1 Running 0 ... meta-loki-node-exporter-b976b 1/1 Running 0 ... meta-loki-node-exporter-vsm4s 1/1 Running 0 ...
Next Steps
You have successfully deployed the Kubernetes Monitoring Helm chart to monitor your Loki cluster. You can now move onto the next step of deploying the Loki mixin to visualize the metrics and logs from your Loki cluster. For more information, see Install the Loki Mixin.
Key metrics for monitoring Loki
Loki exposes many metrics, and each component behaves differently under load. This page focuses on the highest-signal metrics for detecting negative trends early.
{{< admonition type="note" >}} The example queries on this page are PromQL. Run them against the Prometheus-compatible data source where your Loki metrics are stored (for example, Prometheus, Mimir, or Grafana Cloud Metrics). {{< /admonition >}}
For setup and prebuilt dashboards and alerts, refer to:
Request error rate
Watch request failures first. A sustained increase in 5xx responses is usually the earliest sign of user-visible impact.
Key metric:
loki_request_duration_seconds, a histogram with the labelsmethod,route,status_code, andws. Use the_countand_bucketsuffixes to derive request rate and latency percentiles.
{{< admonition type="note" >}}
cluster, namespace, and job in the example queries on this page come from your Prometheus scrape configuration, not from Loki itself. They are not labels on the metric.
{{< /admonition >}}
Example query:
100 * sum(rate(loki_request_duration_seconds_count{status_code=~"5.."}[2m])) by (cluster, namespace, job, route)
/
sum(rate(loki_request_duration_seconds_count[2m])) by (cluster, namespace, job, route)
Abnormal behavior:
- Any sustained increase in 5xx ratio.
- The Loki mixin alert
LokiRequestErrorsfires when this ratio is greater than 10% for 15 minutes.
Request latency (p99)
Latency degradation can appear before hard failures. Track p99 for read and write routes.
Key metric:
loki_request_duration_seconds_bucket(histogram buckets of the same metric described in Request error rate)
Example query:
histogram_quantile(0.99, sum(rate(loki_request_duration_seconds_bucket[1m])) by (le, cluster, namespace, job, route))
Abnormal behavior:
- Rising p99 over time, especially in query-frontend and distributor paths.
- The Loki mixin alert
LokiRequestLatencyfires when p99 exceeds 1 second for 15 minutes. This alert excludes tail and scheduler routes, so the example query above can show a higher p99 than the alert reacts to.
Panics
Panics are high-severity faults and should stay at zero.
Key metric:
loki_panic_total
Example query:
sum(increase(loki_panic_total[10m])) by (cluster, namespace, job)
Abnormal behavior:
- Any value above zero. The Loki mixin alert
LokiRequestPanicstreats this as critical.
Discarded samples
Discarded samples indicate data that Loki rejected or dropped. This is one of the most important ingestion-quality signals.
Key metric:
loki_discarded_samples_total(counter with labels includingtenantandreason, plusretention_hours,policy, andformat)
Example query:
topk(10, sum by (tenant, reason) (rate(loki_discarded_samples_total{cluster="$cluster", namespace="$namespace"}[$__rate_interval])))
Abnormal behavior:
- Increasing discard rate.
- New or growing
reasonvalues (for example, tenant limits or stream limits).
Compaction health
Compaction issues can silently degrade read performance and retention behavior over time.
{{< admonition type="note" >}}
Prior to Loki 4.0 the compaction and retention metrics used a loki_boltdb_shipper_ prefix for historical reasons.
The compactor emitted these metrics regardless of which index type you use, including TSDB.
After removing the legacy BoltDB Shipper index, the prefix has been renamed to loki_tsdb_shipper_.
{{< /admonition >}}
Key metrics:
loki_tsdb_shipper_compactor_runningloki_tsdb_shipper_compact_tables_operation_last_successful_run_timestamp_secondsloki_tsdb_shipper_compact_tables_operation_totalloki_tsdb_shipper_compact_tables_operation_duration_seconds
Example queries:
sum(loki_tsdb_shipper_compactor_running) by (cluster, namespace)
time() - (loki_tsdb_shipper_compact_tables_operation_last_successful_run_timestamp_seconds > 0)
Abnormal behavior:
- More than one compactor running. Only one compactor instance should run at a time. Running more than one can lead to data loss. The Loki mixin alert
LokiTooManyCompactorsRunningfires when more than one compactor runs for 5 minutes. - No successful compaction for multiple hours. The mixin alert
LokiCompactorHasNotSuccessfullyRunCompactiontriggers when the last successful run is more than 3 hours old, and fires after that condition holds for 1 additional hour, so it takes about 4 hours in total to alert.
Ingester health and flush behavior
Ingester pressure often appears as memory growth, poor chunk utilization, or flush backlog.
Key metrics:
loki_ingester_memory_streamsloki_ingester_memory_stream_shardsloki_ingester_memory_chunksloki_ingester_flush_queue_lengthloki_ingester_chunk_utilizationloki_ingester_chunks_flushed_total
Example queries:
sum(loki_ingester_memory_streams{cluster="$cluster", namespace="$namespace"})
sum(loki_ingester_memory_stream_shards{cluster="$cluster", namespace="$namespace"})
/
sum(loki_ingester_memory_streams{cluster="$cluster", namespace="$namespace"})
sum(loki_ingester_flush_queue_length{cluster="$cluster", namespace="$namespace"})
Abnormal behavior:
- Persistent growth in in-memory streams or chunks.
- Increasing flush queue length.
- Low chunk utilization for long periods.
Distributor throughput
Throughput changes help identify upstream sender issues, sudden traffic shifts, or ingestion bottlenecks.
Key metrics:
loki_distributor_bytes_received_totalloki_distributor_lines_received_total
Example queries:
sum(rate(loki_distributor_bytes_received_total{cluster="$cluster", namespace="$namespace"}[$__rate_interval]))
sum(rate(loki_distributor_lines_received_total{cluster="$cluster", namespace="$namespace"}[$__rate_interval]))
Abnormal behavior:
- Sharp drops (possible data path interruption).
- Unexpected spikes (possible overload or noisy tenants).
Kafka-based ingestion (experimental)
{{< admonition type="note" >}} These metrics only apply if you've enabled Kafka-based ingestion, an experimental write-path feature. {{< /admonition >}}
Key metrics:
loki_kafka_client_partition_reader_consumption_lag_seconds: the single most important Kafka health signal. Growing lag means ingesters can't keep up with the rate at which the distributor produces records.loki_distributor_kafka_appends_total(labelstatus): produce success and failure rate. A rising failure rate means writes aren't reaching Kafka.loki_distributor_kafka_latency_seconds: produce latency. A sustained increase points to Kafka broker pressure or network issues.loki_kafka_client_partition_reader_fetch_errors_total: errors fetching records from Kafka brokers. This should stay near zero.
Example queries:
# Consumption lag p99 by partition state
histogram_quantile(0.99, sum(rate(loki_kafka_client_partition_reader_consumption_lag_seconds_bucket{phase="running", cluster="$cluster", namespace="$namespace"}[$__rate_interval])) by (le, cluster, namespace))
# Kafka produce error rate
sum(rate(loki_distributor_kafka_appends_total{status="fail", cluster="$cluster", namespace="$namespace"}[$__rate_interval])) by (cluster, namespace)
Abnormal behavior:
- Consumption lag growing over time (ingesters can't keep up with the produce rate).
- Any sustained produce failures.
- Rising produce latency (broker pressure, network issues, or too few partitions).
- Fetch errors (broker unavailability or a protocol version mismatch).
For a complete list of Kafka metrics, refer to Kafka-based ingestion.
Object store operations
Object store latency and failures directly impact query and retention workflows.
Key metrics:
loki_objstore_bucket_operations_totalloki_objstore_bucket_operation_failures_totalloki_objstore_bucket_operation_duration_seconds
Example queries:
sum by (operation) (rate(loki_objstore_bucket_operation_failures_total{cluster="$cluster", namespace="$namespace"}[$__rate_interval]))
histogram_quantile(0.99, sum(rate(loki_objstore_bucket_operation_duration_seconds_bucket{cluster="$cluster", namespace="$namespace"}[$__rate_interval])) by (le, operation))
Abnormal behavior:
- Growing failure rates by operation.
- Increasing p99 latency for
get,get_range, orupload.
Connection metrics just for S3:
net_conntrack_dialer_conn_attempted_totalnet_conntrack_dialer_conn_established_totalnet_conntrack_dialer_conn_failed_totalnet_conntrack_dialer_conn_closed_total
Abnormal behavior:
- Non-zero rate of failed connections.
- High rate of closed connections under ordinary load.
Resource and runtime health
Resource pressure can explain or predict service degradation before alert thresholds are crossed.
Common signals to track:
- Container CPU usage
- Container memory working set
- Go heap in use
- Disk read and write rates
- Container restarts
Abnormal behavior:
- Repeated restart spikes.
- Sustained CPU saturation.
- Memory growth without recovery.
Loki Canary (end-to-end data verification)
If you run Loki Canary, use it as an end-to-end correctness signal, not only a performance signal.
Key metrics:
loki_canary_missing_entries_totalloki_canary_spot_check_missing_entries_totalloki_canary_response_latency_seconds_bucket
Example query:
sum(increase(loki_canary_missing_entries_total{cluster=~"$cluster", namespace=~"$namespace"}[$__range]))
/
sum(increase(loki_canary_entries_total{cluster=~"$cluster", namespace=~"$namespace"}[$__range]))
* 100
Abnormal behavior:
- Any non-zero missing rate sustained over time.
Internal error log rate
Internal logs provide fast context when metrics indicate degradation.
Key metric:
loki_internal_log_messages_total
Use this metric with component logs to correlate where failures begin.
Retention and sweeper progress
Retention and sweeper lag can cause storage growth and delayed data lifecycle actions.
Key metrics:
loki_compactor_apply_retention_last_successful_run_timestamp_secondsloki_tsdb_shipper_retention_sweeper_marker_file_processing_current_timeloki_tsdb_shipper_retention_sweeper_chunk_deleted_duration_seconds_count
Example query:
time() - (loki_tsdb_shipper_retention_sweeper_marker_file_processing_current_time{cluster="$cluster", namespace="$namespace"} > 0)
Abnormal behavior:
- Increasing sweeper lag.
- Falling delete throughput or sustained delete failures.
Next steps
- Install and keep the latest Loki mixin dashboards and alerts.
- Build component-specific alerts on top of these baseline signals.
- Pair metrics with Loki component logs, including
metrics.go, during incident response.
Install dashboards, alerts, and recording rules
Loki is instrumented to expose metrics about itself via the /metrics endpoint, designed to be scraped by Prometheus. Each Loki release includes a mixin. The Loki mixin provides a set of Grafana dashboards, Prometheus recording rules and alerts for monitoring Loki.
To set up monitoring using the mixin, you need to:
- Deploy the Kubernetes Monitoring Helm chart. Follow the instructions in the Deploy Loki Meta-monitoring documentation.
- Be actively storing metrics from your Loki cluster in Grafana Cloud or a separate LGTM stack.
This procedure assumes that you have set up Loki using the Helm chart.
{{< admonition type="note" >}} Be sure to update the commands and configuration to match your own deployment. {{< /admonition >}}
Install Loki dashboards in Grafana
After Loki metrics are scraped by the Kubernetes Monitoring Helm chart and stored in a Prometheus compatible time-series database, you can monitor Loki’s operation using the Loki mixin.
Each Loki release includes a mixin that includes:
- Relevant dashboards for overseeing the health of Loki as a whole, as well as its individual Loki components
- Recording rules that compute metrics that are used in the dashboards
- Alerts that trigger when Loki generates metrics that are outside of normal parameters
To install the mixins in Grafana and a Prometheus-compatible rules store, the general steps are as follows:
-
Download the mixin dashboards from the Loki repository.
-
Import the dashboards in your Grafana instance.
-
Upload
alerts.yamlandrules.yamlfiles to Prometheus or Mimir withmimirtool.
Download the loki-mixin dashboards
-
First, clone the Loki repository from Github:
git clone https://github.com/grafana/loki cd loki -
Check out the tag that matches the version of Loki you are running, so that the dashboards, alerts, and recording rules match the metrics your Loki cluster exposes. For example, if you are running Loki 3.7.6:
git checkout v3.7.6 -
Once you have a local copy of the repository at the matching version, navigate to the
production/loki-mixin-compileddirectory.cd production/loki-mixin-compiled
This directory contains a compiled version of the alert and recording rules, as well as the dashboards.
{{< admonition type="note" >}}
If you want to change any of the mixins, make your updates in the production/loki-mixin directory.
Use the instructions in the README in that directory to regenerate the files.
{{< /admonition >}}
Import the dashboards to Grafana
The dashboards directory includes the monitoring dashboards that can be installed into your Grafana instance.
Refer to Import a dashboard in the Grafana documentation.
{{< admonition type="note" >}}
The dashboards and alerts expect your metrics to carry these labels: cluster, namespace, job, container, pod, and instance. The Kubernetes Monitoring Helm chart provides them, because it adds the cluster name as a global label and collects container and node metrics using cAdvisor, kube-state-metrics, and Node Exporter.
If your setup uses different label names, or does not collect container and node metrics, some panels and alerts show no data until you relabel your metrics to match. {{< /admonition >}}
{{< admonition type="tip" >}} Install all dashboards. You can only import one dashboard at a time. Create a new folder in the Dashboards area, for example “Loki Monitoring”, as an easy location to save the imported dashboards. {{< /admonition >}}
To create a folder:
- Open your Grafana instance and select Dashboards.
- Click the New button.
- Select New folder from the New menu.
- Name your folder, for example, “Loki Monitoring”.
- Click Create.
To import a dashboard:
- Open your Grafana instance and select Dashboards.
- Click the New button.
- Select Import from the New menu.
- On the Import dashboard screen, select Upload dashboard JSON file.
- Browse to
production/loki-mixin-compiled/dashboardsand select the dashboard to import. Or, drag the dashboard file, for example,loki-operational.json, onto the Upload area of the Import dashboard screen. - Select a folder in the Folder menu where you want to save the imported dashboard. For example, select "Loki Monitoring" created in the earlier steps.
- Click Import.
The imported files are listed in the Loki Monitoring dashboard folder.
To view the dashboards in Grafana:
- Select Dashboards in your Grafana instance.
- Select Loki Monitoring, or the folder where you uploaded the imported dashboards.
- Select any file in the folder to view the dashboard.
Add alerts and recording rules to Prometheus or Mimir
The rules and alerts need to be installed into a Prometheus instance, Mimir or a Grafana Enterprise Metrics cluster.
You can find the YAML files for alerts and rules in the following directories in the Loki repo:
production/loki-mixin-compiled/alerts.yaml(Optional)production/loki-mixin-compiled/rules.yaml(Required for some dashboard panels to show data)
You use mimirtool to load the mixin alerts and rules definitions into a Prometheus instance, Mimir, or a Grafana Enterprise Metrics cluster. The following examples show how to load the mixin alerts and rules into a Grafana Cloud instance.
-
Download mimirtool.
-
Export the authentication credentials for connecting to your Grafana Cloud Mimir instance.
export MIMIR_ADDRESS=<CLOUD-MIMIR-URL> export MIMIR_API_USER=<CLOUD-MIMIR-USER> export MIMIR_API_KEY=<CLOUD-MIMIR-API-KEY> export MIMIR_TENANT_ID=<CLOUD-MIMIR-USER> # Same as MIMIR_API_USER when using Grafana CloudThe best place to locate these credentials is to:
- Sign into Grafana Cloud and create a new access policy.
- In the main menu, select Security > Access Policies.
- Click Create access policy.
- Give the policy a Name and select the following permissions:
- Alerts: Write & Read
- Rules: Write & Read
- Click Create.
- Click Add Token. Give the token a name and click Create.
- Collect
URLanduserfor Prometheus- Navigate to the Grafana Cloud Portal Overview page.
- Click the Details button for your Alerts instance.
- From the Configuring your Alerting Stacks section, collect the instance User and URL for Metrics Authentication Settings.
- Sign into Grafana Cloud and create a new access policy.
-
Using the same terminal we exported the Mimir environment variables into earlier, run the following command to load the recording rules:
mimirtool rules load rules.yaml -
(Optional) Load the alert rules:
mimirtool rules load alerts.yaml
Refer to the mimirtool documentation for more information.
Next steps
After you have installed the Loki mixin dashboards, alerts, and recording rules, you can now monitor your production Loki cluster using Grafana. Make sure you review the mixins when you upgrade Loki to make sure you are using the latest version of the mixin.
You can now move onto:
- Send Logs: Ready to start sending your own logs to Loki, there a several methods you can use. For more information, refer to send data.
- Query Logs: LogQL is an extensive query language for logs and contains many tools to improve log retrieval and generate insights. For more information see the Query section.
- Alert: Lastly you can use the ruler component of Loki to create alerts based on log queries. For more information refer to Alerting.
Single binary meta-monitoring
Meta monitoring for monolithic mode deployments involves some additional configuration. This approach does not use the Kubernetes Monitoring Helm chart.
Metrics
Configure Alloy or Prometheus to scrape the Loki metrics endpoint, adding the additional labels that are expected by the mixin dashboards, alerts, and recording rules:
Alloy
prometheus.scrape "loki" {
targets = [{
__address__ = "localhost:3100",
cluster = "prod",
namespace = "default",
job = "default/loki-single-binary",
pod = "loki-single-binary",
container = "loki",
}]
forward_to = [prometheus.remote_write.default.receiver]
}
prometheus.remote_write "default" {
endpoint {
url = "<PROMETHEUS_REMOTE_WRITE_URL>"
}
}
Prometheus
scrape_configs:
- job_name: loki
static_configs:
- targets: ['localhost:3100']
labels:
cluster: prod
namespace: default
job: default/loki-single-binary
pod: loki-single-binary
container: loki
Mixins
To generate the mixins:
-
Install jb.
-
Install mixtool.
-
Clone the Loki repository from Github.
git clone https://github.com/grafana/loki cd loki -
Check out the tag that matches the version of Loki you are running, so that the dashboards, alerts, and recording rules match the metrics your Loki cluster exposes. For example, if you are running Loki 3.7.6:
git checkout v3.7.6 -
Create the output directory.
mkdir production/loki-mixin-compiled-single-binary -
Navigate to the mixin directory.
cd production/loki-mixin -
Use
jbto install mixin dependencies and generate thevendordirectory.jb install -
Create the mixin configuration file
single-binary.libsonnet.cat <<EOF > single-binary.libsonnet local loki = import 'mixin.libsonnet'; loki + { _config+:: { meta_monitoring+: { enabled: true, }, canary+: { // Whether or not to include the loki-canary dashboard. // Set to false if you don't run loki-canary. enabled: true, }, promtail+: { // Whether or not to include promtail specific dashboards enabled: false, }, // Tunes histogram recording rules to aggregate over this interval. // Set to at least twice the scrape interval; otherwise, recording rules will output no data. // Set to four times the scrape interval to account for edge cases: https://www.robustperception.io/what-range-should-i-use-with-rate/ recording_rules_range_interval: '5m', }, } EOF -
Generate dashboards, alerts, and recording rules.
mixtool generate all \ --output-alerts ../loki-mixin-compiled-single-binary/alerts.yaml \ --output-rules ../loki-mixin-compiled-single-binary/rules.yaml \ --directory ../loki-mixin-compiled-single-binary/dashboards \ single-binary.libsonnet -
See the generated dashboards, alerts, and recording rules in the
production/loki-mixin-compiled-single-binarydirectory. -
Follow the instructions in the Install Mixins documentation to set up the dashboards, alerts, and recordings.
Horizontal scaling of Compactor
{{< admonition type="caution" >}} Compactor horizontal scaling is an experimental feature. Use it with caution in your production environments. {{< /admonition >}}
Grafana Loki saw a major improvement in its operational complexity and cost-effectiveness with the introduction of object-storage-based indexes. This change also led to the addition of a singleton Compactor service, initially responsible only for index compaction. However, as new features like Custom Retention and Deletion of Logs with line filters were introduced, the Compactor's responsibilities grew. With increasing scale and more demanding features, especially log deletion with line filters, the singleton Compactor began to show its scaling limits.
Now, you can run the Loki Compactor in a horizontally scalable mode. Since log deletion with line filters is the compactor's most operationally intensive work, initially, this horizontally scalable architecture will be leveraged to speed up and distribute the workload specifically for its log deletion with line filters functionality.
How compactor horizontal scaling works
We have introduced two new modes to the Compactor for operating in horizontally scalable mode:
-
Main:
- Runs all Compactor functions and distributes chunk processing for log line deletion with filters to the workers.
- Should be deployed as a singleton with access to a disk, like the current singleton deployment.
-
Worker:
- Connects to the Main Compactor over gRPC to get and execute the jobs.
- Multiple replicas can be deployed to achieve higher job processing throughput.
- Only needs access to Object Storage for reading/writing chunks.
Compactor horizontal scaling implementation details
Although, Horizontally Scalable Compactor currently only supports distributing the work of log line deletion with filters, in the future, we might add support for distributing more kind of work to the Workers. We are going to use the current functionality to see in detail some of the implementation details.
Main mode
For Distribution of Chunk processing work from Main compactor, there are following core components involved:
-
Deletion Manifest Builder: The manifest builder works on a batch of delete request to discover all the chunks covered by them based on their labels filters and time range. Using the discovered chunks, it creates structured manifests and stores it to the Object Storage. Here is what comprises Deletion Manifest:
- Segments: Groups of up to 100K chunks per segment. Also acts as partition for chunks by Loki Tenant/Table.
- Manifest: Complete metadata about all segments and requests to process.
-
Job Builder: The job builder converts manifests into discrete jobs and manages their lifecycle:
- Job Creation: Breaks segments into jobs of up to 1K chunks each. Also includes line filters to apply on the chunks to remove the log lines requested for deletion.
- Progress Tracking: Monitors job completion and stops processing of manifest when any of the jobs fail.
- Job Queueing: Sends jobs to the Job Queue for processing.
- Storage Updates: Collects storage updates suggested by Workers and stores them to Object Storage for each segment.
- Post-processing Cleanup: Marks requests as processed and removes all the files from the storage.
-
Job Queue: The job queue manages job distribution and Worker-Job Builder communication.
- Job Distribution: Sends jobs to available workers via gRPC streaming.
- Retry Logic: Automatically retries failed or timed-out jobs until allowed number of attempts.
- Job Response: Sends the job processing response to Job Builder. Also notifies about failed jobs after running out of retries.
Worker mode
Workers connect to the Main Compactor via gRPC to fetch and execute jobs, returning results on the same gRPC stream.
It uses compactor_grpc_address under the common config to connect to the Main Compactor.
When handling Deletion jobs, a worker downloads the listed chunks, applies the specified filters to rebuild them without the filtered lines, and then provides a comprehensive storage update as job execution response. The storage update details which chunks to delete from Object Storage and which newly created chunks to index.
Configure compactor mode
The horizontal_scaling_mode configuration option in the compactor controls the enablement of horizontally scalable compactor deployment.
It supports setting the following modes:
disabled(default): Keeps the horizontal scaling mode disabled. Locally runs all the functions of the compactor.main: Runs all functions of the compactor. Distributes work to workers where possible.worker: Runs the compactor in worker mode, only working on jobs built by the main compactor.
Configure Main mode
To run Compactor in Main mode, the Horizontal Scaling Mode needs to be set to "main":
compactor:
# CLI flag: -compactor.horizontal-scaling-mode
horizontal_scaling_mode: "main"
Additionally, there are the below config options available for the Main compactor to configure some aspects of Job Building:
compactor:
jobs_config:
deletion:
# Object storage path prefix for storing deletion manifests.
# CLI flag: -compactor.jobs.deletion.deletion-manifest-store-prefix
[deletion_manifest_store_prefix: <string> | default = "__deletion_manifest__/"]
# Maximum time to wait for a job before considering it failed and retrying.
# CLI flag: -compactor.jobs.deletion.timeout
[timeout: <duration> | default = 15m]
# Maximum number of times to retry a failed or timed out job.
# CLI flag: -compactor.jobs.deletion.max-retries
[max_retries: <int> | default = 3]
Configure Worker mode
To run Compactor in Worker mode, the Horizontal Scaling Mode needs to be set to "worker" and Main compactor's GRPC address needs to be set:
common:
compactor_grpc_address: <HOST>:<GRPC_PORT>
compactor:
# CLI flag: -compactor.horizontal-scaling-mode
horizontal_scaling_mode: "worker"
Additionally, there are the below config options available for the Worker to configure some aspects of Job execution:
compactor:
jobs_config:
deletion:
# Maximum number of chunks to process concurrently in each worker.
# CLI flag: -compactor.jobs.deletion.chunk-processing-concurrency
[chunk_processing_concurrency: <int> | default = 3]
worker_config:
# Number of sub-workers to run for concurrent processing of jobs. This
# value must be greater than 0, or the compactor fails to start.
# CLI flag: -compactor.worker.num-sub-workers
[num_sub_workers: <int> | default = 4]
Filesystem object store
The filesystem object store is the easiest way to get started with Grafana Loki. This topic describes its pros and cons.
The filesystem object store keeps every object, such as chunks, in a directory that you specify. The recommended way to configure this directory is in the common.storage.filesystem block:
common:
storage:
filesystem:
chunks_directory: /tmp/loki/chunks
rules_directory: /tmp/loki/rules
You can also configure the chunks directory directly under storage_config:
storage_config:
filesystem:
directory: /tmp/loki/
Loki creates a folder for every tenant. All the chunks for one tenant are stored in that tenant's folder.
If you run Loki in single-tenant mode, Loki puts all the chunks in a folder named fake. This is the synthesized tenant name that Loki uses in single-tenant mode.
See multi-tenancy for more information.
{{< admonition type="note" >}}
Loki also supports an alternative, opt-in filesystem client based on thanos-io/objstore. Enable it by setting use_thanos_objstore: true and configuring storage_config.object_store.filesystem.dir. This client will become the default way to configure object store clients in a future release. For configuration examples, see Thanos storage examples and the storage client migration guide.
{{< /admonition >}}
Pros
The filesystem object store is very simple. It requires no additional software to run Loki, and it works with TSDB, which is the recommended index store.
It's great for low volume applications, proof of concepts, and just playing around with Loki.
Cons
Grafana Labs does not support the filesystem object store for production environments, including for customers who have purchased a support contract.
Scaling
At some point there is a limit to how many chunks you can store in a single directory. For example, see issue #1502, which explains how a Loki user ran into a strange error with about 5.5 million chunk files in their file store. That issue also describes a workaround for the problem.
Loki writes one chunk per stream, so keeping the number of active streams low reduces the number of chunk files. You can also tune the following ingester settings to reduce how many chunks get flushed, although lower flush rates trade off for higher memory consumption:
chunk_target_size(default 1.5 MB): a target compressed size for each chunk.max_chunk_age(default 2h): the maximum time a chunk stays in memory before Loki flushes it. Consider increasing this beyond the default.chunk_idle_period(default 30m): how long an idle chunk stays in memory before Loki flushes it. Consider increasing this to matchmax_chunk_age.
It's still possible to store terabytes of log data with the filesystem store, but keep in mind the limitations on how many files a filesystem can efficiently store in a single directory.
Durability
The durability of your objects depends entirely on the underlying filesystem. Other object stores, such as S3 and GCS, do a lot of work behind the scenes to offer much higher durability for your data.
High availability
Running Loki as a cluster is not possible with the filesystem store, unless you share the filesystem in some way, for example over NFS. Using a shared filesystem is likely to give you a poor experience with Loki, just as it does with almost every other application.
Retention and deletion
When you use the filesystem chunk store, Loki does not delete chunks based on disk usage or free space. Loki only deletes data according to your retention configuration, so you must handle disk-full scenarios yourself, outside of Loki.
Retention-based deletion runs through the compactor. When you enable retention with compactor.retention_enabled, you must also set compactor.delete_request_store. Set it to filesystem to store delete requests in this object store.
For more information, see Retention and the storage retention section.
Log entry deletion
Grafana Loki supports the deletion of log entries from a specified stream. Log entries that fall within a specified time window and match an optional line filter are those that will be deleted.
Log entry deletion is supported when the TSDB index is configured as the index store. It is also supported on the deprecated BoltDB Shipper index, but BoltDB Shipper is being removed in Loki 4.0, so new deployments should use TSDB.
The compactor component exposes REST endpoints that process delete requests. Hitting the endpoint specifies the streams and the time window. The deletion of the log entries takes place after a configurable cancellation time period expires.
Log entry deletion relies on configuration of the custom logs retention workflow as defined for the compactor. The compactor looks at unprocessed requests which are past their cancellation period to decide whether a chunk is to be deleted or not.
Configuration
Enable log entry deletion by setting retention_enabled to true in the compactor's configuration (-compactor.retention-enabled on the command line). The tenant's deletion_mode must also be filter-only or filter-and-delete, which is the case by default.
delete_request_store also needs to be configured when retention is enabled to process delete requests, this determines the storage bucket that stores the delete requests.
{{< admonition type="warning" >}}
Be very careful when enabling retention. It is strongly recommended that you also enable versioning on your objects in object storage to allow you to recover from accidental misconfiguration of a retention setting. If you want to enable deletion but do not want to enforce retention, configure the retention_period setting with a value of 0s.
{{< /admonition >}}
deletion_mode is a global and per-tenant setting in limits_config. Its default value is filter-and-delete, so log entry deletion is active for every tenant as soon as retention_enabled is true and delete_request_store is configured, unless you explicitly change the mode. Set deletion_mode in the main configuration file for a global default, or override it per tenant in the runtime configuration file.
deletion_mode supports three values:
disabled: Deletion is not allowed. Requests to the deletion API endpoints are rejected with a403 Forbiddenresponse.filter-only: Log lines matching the query in the delete request are filtered out when querying Loki. They are not removed from storage.filter-and-delete: Log lines matching the query in the delete request are filtered out when querying Loki, and they are also removed from storage. This is the default.
A delete request may be canceled within a configurable cancellation period. Set the delete_request_cancel_period in the compactor's YAML configuration or on the command line when invoking Loki. Its default value is 24h. To cancel a delete request that has already started processing, pass the force=true query parameter to the cancellation endpoint.
Delete requests that include a line filter are split into smaller requests that each cover no more than delete_max_interval (24h by default). You can request a smaller split for a single request with the max_interval query parameter, but it cannot be larger than delete_max_interval. Delete requests without a line filter are not split. Refer to the HTTP API reference for the full set of request parameters.
Delete requests themselves are stored using the database type set by delete_request_store_db_type, which defaults to boltdb. You can instead use sqlite. When you migrate from one database type to another, you can set backup_delete_request_store_db_type to boltdb so that delete requests are also written to a backup database. Refer to the compactor configuration reference for details.
The deletion API endpoints are registered only when compactor.retention_enabled is true. When retention is not enabled, the endpoints are unavailable for every tenant, whatever the deletion_mode value is. When retention is enabled, use the deletion_mode tenant override to control which tenants can use the deletion API.
{{< admonition type="note" >}} Deletion of log lines with line filters is one of the compactor's most resource-intensive operations. If you delete large volumes of data with line filters, refer to Horizontal scaling of Compactor to distribute that work across multiple compactor instances. {{< /admonition >}}
Log retention
Retention in Grafana Loki is achieved through the Compactor.
By default the compactor.retention-enabled flag is not set, so the logs sent to Loki live forever.
{{< admonition type="note" >}} If you have a lifecycle policy configured on the object store, please ensure that it is longer than the retention period. {{< /admonition >}}
Granular retention policies to apply retention at per tenant or per stream level are also supported by the Compactor.
Compactor
The Compactor is responsible for compaction of index files and applying log retention.
{{< admonition type="note" >}} Run the Compactor as a singleton (a single instance). {{< /admonition >}}
The Compactor runs compaction and retention on two independent schedules:
- Compaction runs every
compactor.compaction-interval. - Retention runs every
compactor.apply-retention-interval. If you leave this at its default of0s, Loki sets it to the same value ascompactor.compaction-interval. Whenever the two intervals are equal, Loki extends the retention interval by the smaller of10mor half the interval, so that compaction and retention do not run at the same time. For example, with the defaultcompaction_intervalof10m, retention runs every15m.
If the Compactor falls behind on either schedule, it runs the corresponding operation as soon as possible. Both compaction and retention are idempotent, which means once the action has been performed, if the action is performed multiple times, it has no further effect on logs after the first time it is performed. If the Compactor restarts, it continues from where it left off.
{{< admonition type="note" >}} Changes to your retention period are not retroactive, that is, they are not applied to logs that have already been ingested. {{< /admonition >}}
The Compactor's algorithm to apply retention is as follows:
- For each day or table (one table per day with 24h index period):
- Compact multiple index files in the table into per-tenant index files. Result of compaction is a single index file per tenant per day.
- Traverse the per-tenant index. Use the tenant configuration to identify the chunks that need to be removed.
- Remove the references to the matching chunks from the index and add the chunk references to a marker file on disk.
- Upload the new modified index files.
Chunks are not deleted while applying the retention algorithm on the index. They are deleted asynchronously by a sweeper process
and this delay can be configured by setting -compactor.retention-delete-delay. Marker files are used to keep track of the chunks pending for deletion.
Chunks cannot be deleted immediately for the following reasons:
-
Index Gateway downloads a copy of the index files to serve queries and refreshes them at a regular interval. Having a delay allows the index gateways to pull the modified index file which would not contain any reference to the chunks marked for deletion. Without the delay, index files (that are stale) on the gateways could refer to already deleted chunks leading to query failures.
-
It provides a short window of time in which to cancel chunk deletion in the case of a configuration mistake.
By default, marker files are written to local disk, under <working_directory>/retention/<object_store_type>_<period_from_date>/markers/. Marker files should be stored on a persistent disk to ensure that the chunks pending for deletion are processed even if the Compactor process restarts.
{{< admonition type="note" >}}
Grafana Labs recommends running Compactor as a stateful deployment (StatefulSet when using Kubernetes) with a persistent storage for storing marker files.
{{< /admonition >}}
Alternatively, you can configure the Compactor to store marker files in object storage instead of local disk, by setting compactor.deletion-marker-object-store-prefix (deletion_marker_object_store_prefix) to a prefix that ends with /. When set, the Compactor writes marker files to the same per-period object store bucket used for chunks, under that prefix. On startup it also copies any existing marker files from local disk to that prefix, then removes the local copies. This lets you run the Compactor without a persistent volume for marker files. Leave this setting empty to keep using local disk.
Retention Configuration
This Compactor configuration example activates retention.
compactor:
working_directory: /data/retention
compaction_interval: 10m
retention_enabled: true
retention_delete_delay: 2h
retention_delete_worker_count: 150
delete_request_store: gcs
schema_config:
configs:
- from: "2020-07-31"
index:
period: 24h
prefix: index_
object_store: gcs
schema: v13
store: tsdb
storage_config:
tsdb_shipper:
active_index_directory: /data/index
cache_location: /data/index_cache
gcs:
bucket_name: loki
{{< admonition type="note" >}} Retention is only available if the index period is 24h. Single store TSDB requires a 24h index period. {{< /admonition >}}
retention_enabled should be set to true. Without this, the Compactor will only compact tables.
delete_request_store should be set to configure the store for delete requests. This is required when retention is enabled.
working_directory is the directory where marked chunks and temporary tables will be saved.
compaction_interval dictates how often compaction is applied. If the Compactor falls behind, compaction occurs as soon as possible.
apply_retention_interval dictates how often retention is applied, independently of compaction_interval. Refer to Compactor for how the default value is derived from compaction_interval.
retention_delete_delay is the delay after which the Compactor will delete marked chunks.
retention_delete_worker_count specifies the maximum quantity of goroutine workers instantiated to delete chunks.
A few other options are also relevant when tuning retention:
retention_table_timeoutbounds how long the Compactor spends applying retention and processing delete requests on any single table. It defaults to0s, meaning no timeout. If the work on a table exceeds this timeout, the Compactor logs a warning and stops early rather than failing, and the remaining work is picked up again on a later run.retention_backoff_configcontrols the retry behavior (minimum and maximum backoff, and number of retries) used by the sweeper when it fails to delete a marked chunk.delete_request_store_key_prefixsets the path prefix used to store delete requests withindelete_request_store(defaultindex/).delete_request_store_db_typesets the type of database used to store delete requests (defaultboltdb).
Refer to the Configuration reference for the full list of compactor options and their defaults.
Configuring the retention period
Retention period is configured within the limits_config configuration section.
There are two ways of setting retention policies:
retention_periodwhich is applied globally for all log streams.retention_streamwhich is only applied to log streams matching the selector.
{{< admonition type="note" >}}
Each retention_stream rule's period must be at least 24h. Loki rejects configuration with a shorter value, both in the global retention_stream list and in any per-tenant override. This minimum does not apply to the retention_period setting, either globally or per tenant.
{{< /admonition >}}
This example configures global retention that applies to all tenants (unless overridden by configuring per-tenant overrides):
...
limits_config:
retention_period: 744h
retention_stream:
- selector: '{namespace="dev"}'
priority: 1
period: 24h
runtime_config:
file: /etc/overrides.yaml
...
{{< admonition type="note" >}}
You can only use label matchers in the selector field of a retention_stream definition. Arbitrary LogQL expressions are not supported.
{{< /admonition >}}
Per tenant retention can be defined by configuring runtime overrides. For example:
overrides:
"29":
retention_period: 168h
retention_stream:
- selector: '{namespace="prod"}'
priority: 2
period: 336h
- selector: '{container="loki"}'
priority: 1
period: 72h
"30":
retention_stream:
- selector: '{container="nginx", level="debug"}'
priority: 1
period: 24h
Retention period for a given stream is decided based on the first match in this list:
- If multiple per-tenant
retention_streamselectors match the stream, retention period with the highest priority is picked. - If multiple global
retention_streamselectors match the stream, retention period with the highest priority is picked. This value is not considered if per-tenantretention_streamis set. - If a per-tenant
retention_periodis specified, it will be applied. - The global
retention_periodwill be applied if none of the above match. - If no global
retention_periodis specified, the default value of0sis used, which means logs are kept indefinitely.
{{< admonition type="note" >}}
The larger the priority value, the higher the priority. If two matching retention_stream rules have the same priority, Loki applies the shorter (lowest) of the two periods.
{{< /admonition >}}
Stream matching uses the same syntax as Prometheus label matching:
=: Select labels that are exactly equal to the provided string.!=: Select labels that are not equal to the provided string.=~: Select labels that regex-match the provided string.!~: Select labels that do not regex-match the provided string.
The example configurations defined above will result in the following retention periods:
- For tenant
29:- Streams that have the namespace label
prodwill have a retention period of336h(2 weeks), even if the container label isloki, since the priority of theprodrule is higher. - Streams that have the container label
lokibut are not in the namespaceprodwill have a72hretention period. - For the rest of the streams in this tenant, per-tenant override
retention_periodvalue of168his applied.
- Streams that have the namespace label
- For tenant
30:- Streams that have the label
nginxand leveldebugwill have a retention period of24h. - For the rest of the streams in this tenant the global retention period of
744h, since there is no override specified.
- Streams that have the label
- All tenants except
29and30:- Streams that have the namespace label
devwill have a retention period of24hhours. - Streams except those with the namespace label
devwill have the retention period of744h.
- Streams that have the namespace label
{{< admonition type="note" >}} If you are a Grafana Cloud customer, you can use the config self-serve API to configure your tenant retention. {{< /admonition >}}
Example Configuration
Example configuration with GCS with a 28 day retention:
schema_config:
configs:
- from: 2018-04-15
store: tsdb
object_store: gcs
schema: v13
index:
prefix: loki_index_
period: 24h
storage_config:
tsdb_shipper:
active_index_directory: /loki/index
cache_location: /loki/index_cache
gcs:
bucket_name: GCS_BUCKET_NAME
limits_config:
max_query_lookback: 672h # 28 days
retention_period: 672h # 28 days
compactor:
working_directory: /data/retention
delete_request_store: gcs
retention_enabled: true
Object store lifecycle policies
Loki stores chunks and indexes as objects in the configured object store (S3, GCS, Azure Blob Storage, and so on). Chunks are deleted by the Compactor according to your configured retention, but the Compactor only removes chunk objects: it never deletes the objects that hold cluster state, such as the index or the cluster seed.
If you also configure an object store lifecycle policy on the bucket, scope it carefully. A blanket "delete everything older than N days" rule will eventually remove objects that Loki must keep, which corrupts the store and causes query failures. The note at the top of this page applies here as well: any lifecycle expiration must be longer than your retention period.
{{< admonition type="warning" >}} Never apply a lifecycle rule with an empty prefix (one that targets the whole bucket). Always scope lifecycle rules to the per-tenant chunk prefixes described below. {{< /admonition >}}
Bucket object layout
The following objects are written to the configured bucket. The exact prefixes depend on your schema_config and on whether multi-tenancy is enabled, so confirm them against your own configuration before writing a lifecycle rule.
| Object or prefix | Content | Safe to expire with a lifecycle policy? |
|---|---|---|
<tenant_id>/ (for example, fake/ in single-tenant mode, or your org ID per tenant) |
Chunk objects (the log data). | Yes. These are the only objects a lifecycle rule should target. |
Index objects (under the index path prefix, index/ by default, set by index.path_prefix in schema_config) |
TSDB or BoltDB index files, written by ingesters and rewritten by the Compactor. The index.prefix value (for example index_ or loki_index_) is the table-name prefix used inside this path, not the path itself, so match a lifecycle rule against the path prefix. |
No. Loki manages the index lifecycle internally. Deleting index objects removes references to live chunks and breaks queries. |
loki_cluster_seed.json |
A small file at the bucket root holding the cluster seed used by usage reporting. It is written once and never rotated. | No. A blanket age-based rule will delete it once it ages past the time to live (TTL). |
Delete-request store objects (a delete_requests/ table under delete_request_store_key_prefix, index/ by default, in the bucket configured by delete_request_store) |
The Compactor's database of delete requests submitted through the Log entry deletion API, and their processing status. | No. Removing these makes the Compactor lose track of pending delete requests. |
Deletion marker objects (only present if you set deletion_marker_object_store_prefix; otherwise markers are kept on the Compactor's local disk and never appear in the bucket) |
The Compactor's record of chunks that are marked for deletion but not yet swept. Written under the configured prefix in the same per-period bucket used for chunks. | No. Removing these makes the Compactor lose track of chunks pending deletion. |
| Ruler store objects (if the Ruler is backed by the same bucket) | Recording and alerting rule files. | No. These are configuration, not time-series data. |
Compactor retention versus object store lifecycle policies
Compactor retention and object store lifecycle policies are two independent mechanisms, and they should not be configured in ways that conflict:
- The Compactor is index-aware. It only deletes a chunk after it has removed every reference to that chunk from the index, and it waits
compactor.retention-delete-delay(default2h) before the sweeper deletes the marked chunk, so that index gateways pick up the rewritten index first. - An object store lifecycle policy is index-unaware. It deletes objects purely by age and prefix, with no knowledge of whether a chunk is still referenced by the index.
For most deployments, enabling Compactor retention is sufficient and an object store lifecycle policy is not required. If you do add a lifecycle policy as a cost safety net, set its expiration longer than your retention period plus compactor.retention-delete-delay, so the Compactor always deletes chunks first and the lifecycle rule only ever catches objects the Compactor has already orphaned.
Example: Amazon S3 lifecycle policy
This policy expires only chunk objects under a single tenant prefix. Replace fake/ with your tenant prefix (use one rule per tenant, or your org ID prefix, when multi-tenancy is enabled). The Days value must be larger than your retention period plus the delete delay.
{
"Rules": [
{
"ID": "loki-chunk-expiration",
"Status": "Enabled",
"Filter": {
"Prefix": "fake/"
},
"Expiration": {
"Days": 395
}
}
]
}
Apply it with the AWS CLI:
aws s3api put-bucket-lifecycle-configuration \
--bucket YOUR_LOKI_BUCKET \
--lifecycle-configuration file://lifecycle.json
Example: Google Cloud Storage lifecycle rule
The equivalent GCS rule expires objects by age and prefix. As with S3, scope matchesPrefix to the tenant chunk prefix and keep age larger than your retention period plus the delete delay.
{
"rule": [
{
"action": {"type": "Delete"},
"condition": {
"age": 395,
"matchesPrefix": ["fake/"]
}
}
]
}
Apply it with the Google Cloud CLI:
gcloud storage buckets update gs://YOUR_LOKI_BUCKET --lifecycle-file=lifecycle.json
For Azure Blob Storage, use a management policy with a prefixMatch set to the tenant chunk prefix and the same "expiration must exceed retention plus delete delay" rule.
Single Store TSDB (tsdb)
Starting with Loki v2.8, TSDB is the recommended Loki index. It is heavily inspired by the Prometheus TSDB sub-project. This new index is more efficient, faster, and more scalable. It also resides in object storage like the boltdb-shipper index which preceded it.
For the on-disk binary layout of the index files, see TSDB index format.
Example Configuration
To get started using TSDB, add the following configurations to your config.yaml:
schema_config:
configs:
# Old boltdb-shipper schema. Included in example for reference but does not need changing.
- from: "2023-01-03" # <---- A date in the past
index:
period: 24h
prefix: index_
object_store: gcs
schema: v12
store: boltdb-shipper
# New TSDB schema below
- from: "2023-01-05" # <---- A date in the future
index:
period: 24h
prefix: index_
object_store: gcs
schema: v13
store: tsdb
storage_config:
# Old boltdb-shipper configuration. Included in example for reference but does not need changing.
boltdb_shipper:
active_index_directory: /data/index
build_per_tenant_index: true
cache_location: /data/boltdb-cache
index_gateway_client:
# only applicable if using microservices where index-gateways are independently deployed.
# This example is using kubernetes-style naming.
server_address: dns:///index-gateway.<namespace>.svc.cluster.local:9095
# New tsdb-shipper configuration
tsdb_shipper:
active_index_directory: /data/tsdb-index
cache_location: /data/tsdb-cache
index_gateway_client:
# only applicable if using microservices where index-gateways are independently deployed.
# This example is using kubernetes-style naming.
server_address: dns:///index-gateway.<namespace>.svc.cluster.local:9095
query_scheduler:
# the TSDB index dispatches many more, but each individually smaller, requests.
# We increase the pending request queue sizes to compensate.
max_outstanding_requests_per_tenant: 32768
querier:
# Each `querier` component process runs a number of parallel workers to process queries simultaneously.
# You may want to adjust this up or down depending on your resource usage
# (more available cpu and memory can tolerate higher values and vice versa),
# but we find the most success running at around `16` with tsdb
max_concurrent: 16
Operations
Limits
We've added a user per-tenant limit called tsdb_max_query_parallelism in the limits_config. This functions the same as the prior max_query_parallelism configuration but applies to tsdb queries instead. Since the TSDB index will create many more smaller queries compared to the other index types before it, we've added a separate configuration so they can coexist. This is helpful when transitioning between index types. The default parallelism is 128 which should work well for most cases, but you can extend it globally in the limits_config or per-tenant in the overrides file as needed.
Dynamic Query Sharding
Previously we would statically shard queries based on the index row shards configured here. TSDB does Dynamic Query Sharding based on how much data a query is going to be processing. We additionally store size(KB) and number of lines for each chunk in the TSDB index which is then used by the Query Frontend for planning the query. Based on our experience from operating many Loki clusters, we have configured TSDB to aim for processing 300-600 MBs of data per query shard. This means with TSDB we will be running more, smaller queries.
The per-tenant tsdb_max_bytes_per_shard setting in limits_config controls this target size. It defaults to 600MB. Each time a query would need to process more data than this target, Loki doubles the number of shards for that query. As a result, each shard typically ends up processing between half and all of the target amount. This is why, with the default target of 600MB, query shards typically process between 300 MB and 600 MB of data each.
Loki also supports the following related per-tenant settings in limits_config:
tsdb_sharding_strategy: The sharding strategy to use during query planning. The default ispower_of_two, the strategy described above. An alternative,bounded, is also available; refer to the configuration reference before switching, since not all Loki components in a cluster may support it during a rolling upgrade.tsdb_precompute_chunks: When set totrue, Loki computes chunk references once during query planning instead of resolving them again from the index during query execution. This can reduce the number of index calls a query makes, at the cost of higher memory usage. It defaults tofalse. This setting only takes effect whentsdb_sharding_strategyis set tobounded, because thepower_of_twostrategy does not support precomputed chunk references.
Index Caching not required
TSDB is a compact and optimized format. Loki does not currently use an index cache for TSDB. If you are already using Loki with other index types, it is recommended to keep the index caching until all of your existing data falls out of retention or your configured max_query_lookback under limits_config. After that, we suggest running without an index cache (it isn't used in TSDB).
Write Ahead Log
Ingesters temporarily store data in memory. In the event of a crash, there could be data loss. The Write Ahead Log (WAL) helps fill this gap in reliability.
The WAL in Grafana Loki records incoming data and stores it on the local file system in order to guarantee persistence of acknowledged data in the event of a process crash. Upon restart, Loki will "replay" all of the data in the log before registering itself as ready for subsequent writes. This allows Loki to maintain the performance and cost benefits of buffering data in memory and durability benefits (it won't lose data once a write has been acknowledged).
This section will use Kubernetes as a reference deployment paradigm in the examples.
Disclaimer and WAL nuances
The Write Ahead Log in Loki takes a few particular tradeoffs compared to other WALs you may be familiar with. The WAL aims to add additional durability guarantees, but not at the expense of availability. Particularly, there are two scenarios where the WAL sacrifices these guarantees.
-
Corruption/Deletion of the WAL prior to replaying it
In the event the WAL is corrupted/partially deleted, Loki will not be able to recover all of its data. In this case, Loki will attempt to recover any data it can, but will not prevent Loki from starting.
You can use the Prometheus metric
loki_ingester_wal_corruptions_totalto track and alert when this happens. -
No space left on disk
By default, Loki monitors disk usage on the volume that stores the WAL. If usage reaches 90% of capacity, the ingester rejects new writes instead of accepting data it cannot persist. This check runs every 10 seconds and measures the entire filesystem that holds the WAL directory, not just the size of the WAL files. Because of this, you should not share the WAL volume with other data. Writes resume automatically once disk usage drops back below the threshold. You can change the threshold with the
--ingester.wal-disk-full-thresholdflag, or set it to0to disable this protection. This throttling behavior isn't available on Windows.If you disable this threshold, or if a write reaches the disk in the short window before the check runs, the underlying disk can still become completely full. In that case, Loki will not fail the incoming write, but neither will it log the entry to the WAL. In this case, the persistence guarantees across process restarts will not hold for that entry.
You can use the Prometheus metric
loki_ingester_wal_disk_full_failures_totalto track and alert when this happens. This counter increases both when disk usage crosses the throttling threshold and when a WAL write fails because the disk is completely full, so a nonzero value means the WAL disk needs attention, but it does not count each individual rejected write. You can also monitor the gaugeloki_ingester_wal_disk_usage_percentto see current disk usage and how close it is to the threshold.
Backpressure
The WAL also includes a backpressure mechanism to allow a large WAL to be replayed within a smaller memory bound. This is helpful after bad scenarios (i.e. an outage) when a WAL has grown past the point it may be recovered in memory. In this case, the ingester will track the amount of data being replayed and once it's passed 90% of the ingester.wal-replay-memory-ceiling threshold, will flush to storage. When this happens, it's likely that the Loki attempt to deduplicate chunks via content addressable storage will suffer. We deemed this efficiency loss an acceptable tradeoff considering how it simplifies operation and that it should not occur during regular operation (rollouts, rescheduling) where the WAL can be replayed without triggering this threshold. Setting ingester.wal-replay-memory-ceiling to 0 disables backpressure entirely.
Metrics
The following metrics are available for monitoring the WAL:
loki_ingester_wal_corruptions_total: Total number of WAL corruptions encounteredloki_ingester_wal_disk_full_failures_total: Increments when disk usage crosses the--ingester.wal-disk-full-thresholdthrottling threshold, and when a WAL write fails because the disk is completely full. It does not count each individual throttled write.loki_ingester_wal_disk_usage_percent: Current disk usage, from 0.0 to 1.0, for the volume that stores the WAL. This is only reported when--ingester.wal-disk-full-thresholdis greater than0.loki_ingester_wal_records_logged_total: Counter for WAL records loggedloki_ingester_wal_logged_bytes_total: Total bytes written to WALloki_ingester_wal_replay_active: Set to 1 while an ingester is replaying its WAL and checkpoint on startup, 0 otherwiseloki_ingester_wal_replay_duration_seconds: Time taken to replay the checkpoint and the WAL
Changes to deployment
-
Since ingesters need to have the same persistent volume across restarts/rollout, all the ingesters should be run on StatefulSet with fixed volumes.
-
The following flags are relevant when deploying with a persistent volume:
--ingester.wal-enabledenables writing to WAL during ingestion. It defaults totrue, so this typically doesn't need to be set explicitly.--ingester.wal-dirsets the directory where the WAL data should be stored and/or recovered from. Note that this should be on the mounted volume.--ingester.checkpoint-durationto the interval at which checkpoints should be created.--ingester.wal-replay-memory-ceiling(default 4GB) may be set higher/lower depending on your resource settings. It handles memory pressure during WAL replays, allowing a WAL many times larger than available memory to be replayed. This is provided to minimize reconciliation time after very bad situations, i.e. an outage, and will likely not impact regular operations/rollouts at all. We suggest setting this to a high percentage (~75%) of available memory.--ingester.wal-disk-full-threshold(default 0.90) sets the fraction of disk usage (0.0 to 1.0) at which the ingester throttles incoming writes to protect WAL durability. Set to0to disable this protection. For details, refer to Disclaimer and WAL nuances.
Changes in lifecycle when WAL is enabled
Flushing of data to chunk store during rollouts or scale down is disabled. This is because during a rollout of statefulset there are no ingesters that are simultaneously leaving and joining, rather the same ingester is shut down and brought back again with updated config. Hence flushing is skipped and the data is recovered from the WAL. If you need to ensure that data is always flushed to the chunk store when your pod shuts down, you can set the --ingester.flush-on-shutdown flag to true.
Disk space requirements
Based on tests in real world:
- Numbers from an ingester with 5000 series ingesting ~5mb/s.
- Checkpoint period was 5mins.
- disk utilization on a WAL-only disk was steady at ~10-15GB.
You should not target 100% disk utilization.
Migrating from stateless deployments
The ingester Deployment without WAL and StatefulSet with WAL should be scaled down and up respectively in sync without transfer of data between them to ensure that any ingestion after migration is reliable immediately.
Let's take an example of 4 ingesters. The migration would look something like this:
- Bring up one stateful ingester
ingester-0and wait until it's ready (accepting read and write requests). - Scale down the old ingester deployment to 3 and wait until the leaving ingester flushes all the data to chunk store.
- Once that ingester has disappeared from
kc get pods ..., add another stateful ingester and wait until it's ready. Now you haveingester-0andingester-1. - Repeat step 2 to reduce remove another ingester from old deployment.
- Repeat step 3 to add another stateful ingester. Now you have
ingester-0 ingester-1 ingester-2. - Repeat step 4 and 5, and now you will finally have
ingester-0 ingester-1 ingester-2 ingester-3.
How to scale up/down
Scale up
Scaling up is same as what you would do without WAL or StatefulSets. Nothing to change here.
Scale down
When scaling down, we must ensure existing data on the leaving ingesters are flushed to storage instead of just the WAL. This is because we won't be replaying the WAL on an ingester that will no longer exist and we need to make sure the data is not orphaned.
Consider you have 4 ingesters ingester-0 ingester-1 ingester-2 ingester-3 and you want to scale down to 2 ingesters, the ingesters which will be shut down according to StatefulSet rules are ingester-3 and then ingester-2.
Hence before actually scaling down in Kubernetes, port forward those ingesters and hit the /ingester/shutdown?flush=true endpoint. This will flush the chunks and remove itself from the ring, after which it will register as unready and may be deleted.
After hitting the endpoint for ingester-2 ingester-3, scale down the ingesters to 2.
Also you can set the --ingester.flush-on-shutdown flag to true. This enables chunks to be flushed to long-term storage when the ingester is shut down.
Additional notes
Kubernetes hacking
StatefulSets are significantly more cumbersome to work with, upgrade, and so on. Much of this stems from immutable fields on the specification. For example, if one wants to start using the WAL with single store Loki and wants separate volume mounts for the WAL and the TSDB, you may see immutability errors when attempting updates the Kubernetes StatefulSets.
In this case, try kubectl -n <namespace> delete sts ingester --cascade=false.
This will leave the Pods alive but delete the StatefulSet.
Then you may recreate the (updated) StatefulSet and one-by-one start deleting the ingester-0 through ingester-n Pods in that order, allowing the StatefulSet to spin up new pods to replace them.
Scaling Down Using /flush_shutdown Endpoint and Lifecycle Hook
-
StatefulSets for Ordered Scaling Down: The Loki ingesters should be scaled down one by one, which is efficiently handled by Kubernetes StatefulSets. This ensures an ordered and reliable scaling process, as described in the Deployment and Scaling Guarantees documentation.
-
Using PreStop Lifecycle Hook: During the Pod scaling down process, the PreStop lifecycle hook triggers the
/flush_shutdownendpoint on the ingester. This action flushes the chunks and removes the ingester from the ring, allowing it to register as unready and become eligible for deletion. -
Using terminationGracePeriodSeconds: Provides time for the ingester to flush its data before being deleted, if flushing data takes more than 30 minutes, you may need to increase it.
-
Cleaning Persistent Volumes: Persistent volumes are automatically cleaned up by leveraging the enableStatefulSetAutoDeletePVC feature in Kubernetes.
By following the above steps, you can ensure a smooth scaling down process for the Loki ingesters while maintaining data integrity and minimizing potential disruptions.
Non-Kubernetes or baremetal deployments
- When the ingester restarts for any reason (upgrade, crash, etc), it should be able to attach to the same volume in order to recover back the WAL and tokens.
- 2 ingesters should not be working with the same volume/directory for the WAL.
- A rollout should bring down an ingester completely and then start the new ingester, not the other way around.
Troubleshoot Logs Drilldown
Troubleshoot log ingestion (WRITE)
This guide helps you troubleshoot errors that occur when ingesting logs into Loki and writing logs to storage. When Loki rejects log ingestion requests, it's typically due to exceeding rate limits, violating validation rules, or encountering storage issues.
Before you begin, ensure you have the following:
- Access to Grafana Loki logs and metrics
- Understanding of your log ingestion pipeline (Alloy or custom clients)
- Permissions to configure limits and settings if needed
Monitoring ingestion errors
All ingestion errors are tracked using Prometheus metrics:
loki_discarded_samples_total- Count of discarded log samples by reasonloki_discarded_bytes_total- Volume of discarded log data by reason
The reason label on these metrics indicates the specific error type. Set up alerts on these metrics to detect ingestion problems.
Rate limit errors
Rate limits protect Loki from being overwhelmed by excessive log volume. When rate limits are exceeded, Loki returns HTTP status code 429 Too Many Requests.
Error: rate_limited
Error message:
ingestion rate limit exceeded for user <tenant> (limit: <limit> bytes/sec) while attempting to ingest <lines> lines totaling <bytes> bytes, reduce log volume or contact your Loki administrator to see if the limit can be increased
Cause:
The tenant has exceeded their configured ingestion rate limit. This is a global per-tenant limit enforced by the distributor.
Default configuration:
ingestion_rate_mb: 4 MB/secingestion_burst_size_mb: 6 MBingestion_rate_strategy: "global" (shared across all distributors)
Resolution:
-
Increase rate limits (if you have sufficient cluster resources):
limits_config: ingestion_rate_mb: 8 ingestion_burst_size_mb: 12 -
Reduce log volume by:
- Filtering unnecessary log lines using Alloy processing stages
- Collecting logs from fewer targets
- Sampling high-volume streams
- Using Alloy's rate limiting and filtering capabilities
-
Switch to local strategy (if using single-region deployment):
limits_config: ingestion_rate_strategy: local{{< admonition type="note" >}} This multiplies the effective limit by the number of distributor replicas. {{< /admonition >}}
Properties:
- Enforced by: Distributor
- Retryable: Yes
- HTTP status: 429 Too Many Requests
- Configurable per tenant: Yes
Error: per_stream_rate_limit
Error message:
Per stream rate limit exceeded (limit: <limit>/sec) while attempting to ingest for stream '<stream_labels>' totaling <bytes>, consider splitting a stream via additional labels or contact your Loki administrator to see if the limit can be increased
Cause:
A single stream has exceeded its own rate limit. Each stream has a rate limit applied to it to prevent individual streams from overwhelming the set of ingesters it is distributed to. This limit is enforced by the ingester, not the distributor.
Default configuration:
per_stream_rate_limit: 3 MB/secper_stream_rate_limit_burst: 15 MB
Resolution:
-
Split the stream into several smaller streams by adding more distinguishing labels, so the write volume is spread across more streams.
-
Reduce the rate of writes to the stream, for example with an Alloy
stage.limitblock. -
Increase the per-stream limits (we typically recommend no higher than 5MB for
per_stream_rate_limitand 20MB forper_stream_rate_limit_burst):limits_config: per_stream_rate_limit: 5MB per_stream_rate_limit_burst: 20MB
Properties:
- Enforced by: Ingester
- Retryable: Yes
- HTTP status: 429 Too Many Requests
- Configurable per tenant: Yes
Error: stream_limit
Error message:
maximum active stream limit exceeded when trying to create stream <stream_labels>, reduce the number of active streams (reduce labels or reduce label values), or contact your Loki administrator to see if the limit can be increased, user: <tenant>
Cause:
The tenant has reached the maximum number of active streams. Active streams are held in memory on ingesters, and excessive streams can cause out-of-memory errors.
{{< admonition type="note" >}} Under normal conditions, this limit only blocks the creation of genuinely new streams. Log lines for streams that are already active continue to be accepted.
However, during ingester scaling (autoscaling, rollouts, or ring changes such as a partition-ingester migration), streams can be rebalanced to ingesters that were not previously handling them. The new ingester has no record of that stream in memory, so it treats the next write for that stream as a stream creation and checks it against its local share of the limit. If that share is already used up, for example because the same tenant is also adding new high-cardinality streams during the rebalance, writes to the already-active stream can be rejected with stream_limit, even though the tenant has not added new label cardinality for it.
This effect is temporary. It is bounded by the number of ingesters added or restarted, and it typically resolves once the ring is stable and stream ownership is recalculated. If you see stream_limit errors that line up with ingester scaling or rollouts rather than with new label values, this is the likely cause.
{{< /admonition >}}
Default configuration:
max_global_streams_per_user: 5000 (globally)max_streams_per_user: 0 (no local limit per ingester)- Active stream window:
chunk_idle_period(default: 30 minutes)
Terminology:
- Active stream: A stream that has received logs within the
chunk_idle_period(default: 30 minutes)
Resolution:
-
Reduce stream cardinality by:
- Using fewer unique label combinations by removing unnecessary labels
- Avoiding high-cardinality labels (IDs, timestamps, UUIDs)
- Using structured metadata instead of labels for high-cardinality data
-
Increase stream limits (if you have sufficient memory):
limits_config: max_global_streams_per_user: 10000
{{< admonition type="note" >}} Do not increase stream limits to accommodate high cardinality labels, this can result in Loki flushing extremely high numbers of small files which will make for extremely poor query performance. As volume and the size of the infrastructure being monitored increase, it would be expected to increase the stream limit. However even for hundreds of TBs per day of logs you should avoid exceeding 300,000 max global streams per user. {{< /admonition >}}
- Check for a recent ingester rollout or scaling event before increasing limits. If
stream_limiterrors started when ingesters were added, restarted, or migrated, the tenant may not have a real cardinality problem. Increasing the limit will not prevent this transient effect and can mask genuine cardinality growth.
Properties:
- Enforced by: Ingester
- Retryable: Yes
- HTTP status: 429 Too Many Requests
- Configurable per tenant: Yes
Validation errors
Validation errors occur when log data doesn't meet Loki's requirements. These return HTTP status code 400 Bad Request and are not retryable.
Error: line_too_long
Error message:
max entry size <max_size> bytes exceeded for stream <stream_labels> while adding an entry with length <entry_size> bytes
Cause:
A log line exceeds the maximum allowed size.
Default configuration:
max_line_size: 256 KBmax_line_size_truncate: false
Resolution:
-
Truncate long lines instead of discarding them:
limits_config: max_line_size: 256KB max_line_size_truncate: true -
Increase the line size limit (not recommended above 256KB):
limits_config: max_line_size: 256KB{{< admonition type="warning" >}} Loki was built as a large multi-user, multi-tenant database and as such this limit becomes very important to maintain stability and performance with many users query the database simultaneously. We strongly recommend against increasing the max_line_size, doing so will make it very difficult to provide consistent query performance and stability of the system without having to throw extremely large amounts of memory and/or increasing the GRPC message size limits to really high levels, both of which will likely lead to poorer performance and worse experiences. {{< /admonition >}}
-
Filter or truncate logs before sending using Alloy processing stages:
stage.replace { expression = "^(.{10000}).*$" replace = "$1" }
Properties:
- Enforced by: Distributor
- Retryable: No
- HTTP status: 400 Bad Request
- Configurable per tenant: Yes
Error: invalid_labels
Error message:
error parsing labels <labels> with error: <parse_error>
Cause:
Label names or values contain invalid characters or don't follow Prometheus naming conventions:
- Label names must match regex:
[a-zA-Z_][a-zA-Z0-9_]* - Label names cannot start with
__(reserved for Grafana internal use) - Labels must be valid UTF-8
- Properly escape special characters in label values
- Ensure label syntax follows PromQL format:
{key="value"}
Default configuration:
This validation is always enabled and cannot be disabled.
Resolution:
-
Fix label names in your log shipping configuration:
# Incorrect labels: "123-app": "value" # Starts with number "app-name": "value" # Contains hyphen "__internal": "value" # Reserved prefix # Correct labels: app_123: "value" app_name: "value" internal_label: "value" -
Use Alloy's relabeling stages to fix labels:
stage.label_drop { values = ["invalid_label"] } stage.labels { values = { app_name = "", } }
Properties:
- Enforced by: Distributor
- Retryable: No
- HTTP status: 400 Bad Request
- Configurable per tenant: No
Error: missing_labels
Error message:
error at least one label pair is required per stream
Cause:
A log stream was submitted without any labels. Loki requires at least one label pair for each log stream.
Default configuration:
This validation is always enabled and cannot be disabled. Every stream must have at least one label.
Resolution:
Ensure all log streams have at least one label. Update your log shipping configuration to include labels:
loki.write "default" {
endpoint {
url = "http://loki:3100/loki/api/v1/push"
}
}
loki.source.file "logs" {
targets = [
{
__path__ = "/var/log/*.log",
job = "myapp",
environment = "production",
},
]
forward_to = [loki.write.default.receiver]
}
Properties:
- Enforced by: Distributor
- Retryable: No
- HTTP status: 400 Bad Request
- Configurable per tenant: No
Error: too_far_behind
These errors occur when log entries arrive in the wrong chronological order.
Error messages:
entry too far behind, entry timestamp is: <timestamp>, oldest acceptable timestamp is: <cutoff>
Cause:
Logs are being ingested with timestamps that violate Loki's ordering constraints:
- Logs older than half of
max_chunk_agefrom the newest entry are rejected
Default configuration:
unordered_writes: true (since Loki 2.4). This flag, and-ingester.unordered-writes, are deprecated and will be removed in a future major release, after which out-of-order writes will be accepted unconditionally.max_chunk_age: 2 hours- Acceptable timestamp window: 1 hour behind the newest entry (half of
max_chunk_age)
Resolution:
-
Ensure timestamps are assigned correctly:
- Use a timestamp parsing stage in Alloy if logs have embedded timestamps
- Avoid letting Alloy assign timestamps at read time for delayed log delivery
stage.timestamp { source = "timestamp_field" format = "RFC3339" } -
Check for clock skew between log sources and Loki. Ensure clocks are synchronized across your infrastructure.
Properties:
- Enforced by: Ingester
- Retryable: No
- HTTP status: 400 Bad Request
- Configurable per tenant: Yes, for
unordered_writes. No, formax_chunk_age(global only).
Error: greater_than_max_sample_age
Error message:
entry for stream <stream_labels> has timestamp too old: <timestamp>, oldest acceptable timestamp is: <cutoff>
Cause:
The log entry's timestamp is older than the configured maximum sample age. This prevents ingestion of very old logs.
Default configuration:
reject_old_samples: truereject_old_samples_max_age: 168 hours (7 days)
Resolution:
-
Increase the maximum sample age:
limits_config: reject_old_samples_max_age: 336h # 14 days -
Fix log delivery delays causing old timestamps.
-
For historical log imports, temporarily disable the check per tenant.
-
Check for clock skew between log sources and Loki. Ensure clocks are synchronized across your infrastructure.
-
Disable old sample rejection (not recommended):
limits_config: reject_old_samples: false
Properties:
- Enforced by: Distributor
- Retryable: No
- HTTP status: 400 Bad Request
- Configurable per tenant: Yes
Error: too_far_in_future
Error message:
entry for stream <stream_labels> has timestamp too new: <timestamp>
Cause:
The log entry's timestamp is further in the future than the configured grace period allows.
Default configuration:
creation_grace_period: 10 minutes
Resolution:
-
Check for clock skew between log sources and Loki. Ensure clocks are synchronized across your infrastructure.
-
Verify application timestamps Validate timestamp generation in your applications.
-
Verify timestamp parsing in your log shipping configuration.
Properties:
- Enforced by: Distributor
- Retryable: No
- HTTP status: 400 Bad Request
- Configurable per tenant: Yes
Error: max_label_names_per_series
Error message:
entry for stream <stream_labels> has <count> label names; limit <limit>
Cause:
The stream has more labels than allowed.
Default configuration:
max_label_names_per_series: 15
Resolution:
- Reduce the number of labels by:
- Using structured metadata for high-cardinality data
- Removing unnecessary labels
- Combining related labels
{{< admonition type="warning" >}}
We strongly recommend against increasing max_label_names_per_series, doing so creates a larger index which hurts query performance as well as opens the door for cardinality explosions. You should be able to categorize your logs with 15 labels or typically much less. In all our years of running Loki, out of thousands of requests to increase this value, the number of valid exceptions we have seen can be counted on one hand.
{{< /admonition >}}
Properties:
- Enforced by: Distributor
- Retryable: No
- HTTP status: 400 Bad Request
- Configurable per tenant: Yes
Error: label_name_too_long
Error message:
stream <stream_labels> has label name too long: <label_name>
Cause:
A label name exceeds the maximum allowed length.
Default configuration:
max_label_name_length: 1024 bytes
Resolution:
- Shorten label names in your log shipping configuration to under the configured limit. You can use abbreviations or shorter descriptive names.
Properties:
- Enforced by: Distributor
- Retryable: No
- HTTP status: 400 Bad Request
- Configurable per tenant: Yes
Error: label_value_too_long
Error message:
stream <stream_labels> has label value too long: <label_value>
Cause:
A label value exceeds the maximum allowed length.
Default configuration:
max_label_value_length: 2048 bytes
Resolution:
-
Shorten label values in your log shipping configuration to under the configured limit. You can use hash values for very long identifiers.
-
Use structured metadata for long values instead of labels.
Properties:
- Enforced by: Distributor
- Retryable: No
- HTTP status: 400 Bad Request
- Configurable per tenant: Yes
Error: duplicate_label_names
Error message:
stream <stream_labels> has duplicate label name: <label_name>
Cause:
The stream has two or more labels with identical names.
Default configuration:
This validation is always enabled and cannot be disabled. Duplicate label names are never allowed.
Resolution:
-
Remove duplicates Remove duplicate label definitions in your log shipping configuration to ensure unique label names per stream.
-
Check clients Check your ingestion pipeline for label conflicts.
-
Verify processing Verify your label processing and transformation rules.
Properties:
- Enforced by: Distributor
- Retryable: No
- HTTP status: 400 Bad Request
- Configurable per tenant: No
Error: request_body_too_large
Error message:
Request body too large: <size> bytes, limit: <limit> bytes
Cause:
The HTTP compressed push request body exceeds the configured limit in your gateway/reverse proxy or service that fronts Loki after decompression.
Default configuration:
- Loki server max request body size: Configured at the HTTP server level
- Nginx gateway (Helm):
gateway.nginxConfig.clientMaxBodySize(default: 4 MB) - Alloy default batch size: 1 MB
Resolution:
-
Split large batches into smaller, more frequent requests.
-
Increase batch limit Increase the allowed body size on your gateway/reverse proxy. For example, in the Helm chart set
gateway.nginxConfig.clientMaxBodySize; default is 4M.
Properties:
- Enforced by: Distributor
- Retryable: No
- HTTP status: 413 Request Entity Too Large
- Configurable per tenant: No
Storage errors
Storage errors occur when Loki cannot write data to its storage backend or the Write-Ahead Log (WAL).
Error: Disk space full
Error message:
no space left on device
Cause:
Local storage has run out of space. This can affect ingester WAL, chunk cache, or temporary files.
Default configuration:
Disk space is not limited by Loki configuration; it depends on your infrastructure provisioning.
Resolution:
- Free up disk space on Loki instances.
- Configure retention policies to remove old data.
- Scale storage capacity by adding more disk space.
- Monitor disk usage and set up alerts before reaching critical levels.
- Review compactor settings to ensure old data is being cleaned up.
Properties:
- Enforced by: Operating system
- Retryable: Yes (after freeing space)
- HTTP status: 500 Internal Server Error
- Configurable per tenant: No
Error: Chunk write failure
Error message:
failed to store chunks: storage unavailable
Common causes and errors:
- S3/Object Storage errors:
NoSuchBucket: Storage bucket doesn't existAccessDenied: Invalid credentials or permissionsRequestTimeout: Network or storage latency issues
Resolution:
-
Verify storage configuration:
storage_config: aws: s3: s3://region/bucket-name s3forcepathstyle: true -
Check credentials and permissions:
- Ensure service account has write permissions
- Verify IAM roles for S3
- Check API keys for cloud storage
-
Monitor storage health:
- Check object storage availability
-
Review storage metrics:
loki_ingester_chunks_flushed_totalloki_ingester_chunks_flush_errors_total
-
Increase retries and timeouts:
storage_config: aws: s3: http_config: idle_conn_timeout: 90s response_header_timeout: 0s
Properties:
- Enforced by: Ingester (during chunk flush)
- Retryable: Yes (with backoff)
- HTTP status: 500 Internal Server Error
- Configurable per tenant: No
Error: WAL disk full
Error logged:
Error writing to WAL, disk full, no further messages will be logged for this error
Metric: loki_ingester_wal_disk_full_failures_total
Cause:
By default, the ingester monitors disk usage on the volume that stores the WAL and rejects new writes once usage reaches 90% of capacity (-ingester.wal-disk-full-threshold), so that it never accepts data it cannot persist. This error and the associated log message describe a different, rarer case: the disk still filled up completely, either because this throttling is disabled (threshold set to 0), or because a write reached the disk in the short window before the check could catch it. In that case, the ingester continues accepting writes but doesn't log them to the WAL, losing durability guarantees for those entries.
Default configuration:
- WAL enabled by default
- WAL location: configured by
-ingester.wal-dir - Checkpoint interval:
ingester.checkpoint-duration(default: 5 minutes) - Disk full throttling threshold:
ingester.wal-disk-full-threshold(default: 0.9, meaning 90%)
Resolution:
-
Increase disk space for the WAL directory.
-
Monitor disk usage and set up alerts:
loki_ingester_wal_disk_full_failures_total > 0This counter increases both when disk usage crosses the throttling threshold and when a WAL write fails because the disk is completely full. It does not count each individual throttled write, so also watch the gauge
loki_ingester_wal_disk_usage_percentto see how close the disk is to the threshold. -
Reduce log volume to decrease WAL growth.
-
Check WAL checkpoint frequency:
ingester: checkpoint_duration: 5m -
Verify WAL cleanup is working - old segments should be deleted after checkpointing.
Properties:
- Enforced by: Ingester
- Retryable: N/A (writes are accepted but not persisted to WAL)
- HTTP status: N/A (writes succeed, but durability is compromised)
- Configurable per tenant: No
{{< admonition type="note" >}}
This section describes the disk becoming completely full. In normal operation, the ingester's disk usage throttle (-ingester.wal-disk-full-threshold, default 90%) rejects writes to that ingester before the disk actually fills up, so that durability guarantees are preserved. Because Loki replicates each write to multiple ingesters, a client write can still succeed even while one ingester is throttled. Use metric loki_ingester_wal_disk_usage_percent to monitor disk usage.
{{< /admonition >}}
Error: WAL corruption
Error message:
encountered WAL read error, attempting repair
Metric: loki_ingester_wal_corruptions_total
Cause:
The WAL has become corrupted, possibly due to:
- Disk errors
- Process crashes during write
- Filesystem issues
- Partial deletion of WAL files
Default configuration:
- WAL enabled by default
- Automatic repair attempted on startup
- WAL location: configured by
-ingester.wal-dir
Resolution:
-
Monitor for corruption:
increase(loki_ingester_wal_corruptions_total[5m]) > 0 -
Automatic recovery: Loki attempts to recover readable data and continues starting.
-
Investigate root cause:
- Check disk health
- Review system logs for I/O errors
- Verify filesystem integrity
Properties:
- Enforced by: Ingester (on startup)
- Retryable: N/A (Loki attempts automatic recovery)
- HTTP status: N/A (occurs during startup, not request handling)
- Configurable per tenant: No
{{< admonition type="note" >}} Loki prioritizes availability over complete data recovery. The replication factor provides redundancy if one ingester loses WAL data. Recovered data may be incomplete if corruption is severe. {{< /admonition >}}
Network and connectivity errors
These errors occur due to network issues or service unavailability.
Error: Connection refused
Error message:
connection refused when connecting to loki:3100
Cause:
The Loki service is unavailable or not listening on the expected port.
Default configuration:
- Loki HTTP listen port: 3100 (
-server.http-listen-port) - Loki gRPC listen port: 9095 (
-server.grpc-listen-port)
Resolution:
- Verify Loki is running and healthy.
- Check network connectivity between client and Loki.
- Confirm the correct hostname and port configuration.
- Review firewall and security group settings.
- Check for CPU starvation as a Loki pod that is overwhelmed could fail to respond to a request.
Properties:
- Enforced by: Network/OS
- Retryable: Yes (after service recovery)
- HTTP status: N/A (connection-level error)
- Configurable per tenant: No
Error: Context deadline exceeded
Error message:
context deadline exceeded
Cause:
Requests are timing out due to slow response times or network issues.
Default configuration:
- Alloy default timeout: 10 seconds
- Loki server timeout:
-server.http-server-write-timeout(default: 30s)
Resolution:
- Increase timeout values in your ingestion client.
- Check Loki performance and resource utilization.
- Review network latency between client and server.
- Scale Loki resources if needed.
Properties:
- Enforced by: Client/Server
- Retryable: Yes
- HTTP status: 504 Gateway Timeout (or client-side timeout)
- Configurable per tenant: No
Error: Client canceled request (Code 499)
Error message:
level=warn method=/logproto.Querier/GetChunkIDs duration=<duration> msg=gRPC err="rpc error: code = Code(499) desc = the request was cancelled by the client"
Cause:
A downstream client (such as Grafana, a load balancer, or another Loki component) canceled a gRPC request before the server finished processing it. Code(499) is a non-standard gRPC status code derived from the HTTP 499 status ("Client Closed Request"), which Loki uses internally when it detects a Go context.Canceled error. Because it is not the standard gRPC Canceled code (code 1), the gRPC logging middleware logs it at warn level instead of debug level.
The method field indicates which gRPC operation was interrupted. Common methods include /logproto.Querier/GetChunkIDs, /logproto.Querier/Query, and /logproto.Querier/Series. Although these are query-path operations, the error appears in ingester logs because ingesters serve gRPC read requests from queriers.
Common triggers include:
- A user cancels a running query in Grafana
- A query timeout is reached in the query frontend, query scheduler, or a reverse proxy
- The query frontend cancels redundant split or sharded sub-queries after it has enough results
- A load balancer or ingress controller closes the connection due to its own timeout settings
Default configuration:
- Per-tenant query timeout:
query_timeoutinlimits_config/-querier.query-timeout(default: 1m) - Loki server HTTP write timeout:
-server.http-server-write-timeout(default: 30s) - Load balancer / reverse proxy timeout: depends on your infrastructure
Resolution:
-
Determine whether this is expected behavior. Occasional Code(499) warnings are normal in production and indicate that a client disconnected before the server responded. These are safe to ignore if they occur infrequently.
-
Investigate frequent occurrences. If you see a high rate of Code(499) errors, check for:
- Queries that are too broad or slow, causing clients to time out
- Timeout mismatches between layers (for example, a load balancer timeout shorter than the Loki query timeout)
- Overloaded ingesters or queriers that cannot respond quickly enough
-
Align timeouts across your stack. Ensure that timeouts increase from outer layers inward so that Loki has time to respond before upstream components give up:
Grafana timeout > Load balancer timeout > Query frontend timeout > Querier timeout -
Check query performance. Use Loki metrics to identify slow queries:
histogram_quantile(0.99, sum(rate(loki_request_duration_seconds_bucket[5m])) by (le, route)) -
Reduce log noise. If the warnings are expected and you want to reduce log volume, increase the log level for the affected component to
errorusing-log.level=error. Note that this suppresses all warn-level messages, not just Code(499) warnings.
Properties:
- Enforced by: gRPC logging middleware (dskit)
- Retryable: N/A (the original client is no longer waiting for a response)
- HTTP status: 499 (non-standard, client closed request)
- Configurable per tenant: No
Error: Kafka producer backpressure
These errors occur only when the distributor is configured to write to Kafka (-distributor.kafka-writes-enabled=true),
and indicate that the Kafka producer could not keep up with the incoming write volume.
Error messages:
records have timed out before they were able to be producedthe maximum amount of records are buffered, cannot buffer more
Cause:
The distributor's Kafka producer wasn't able to deliver records to the Kafka brokers within kafka_config.write_timeout,
or the amount of unacknowledged, buffered data reached kafka_config.producer_max_buffered_bytes. Both are transient
backpressure conditions: the Kafka brokers or the network path to them are slower than the rate at which the distributor
is trying to produce records, for example during a broker rollout, a partition rebalance, or a broker-side incident.
Default configuration:
kafka_config.write_timeout(-kafka.write-timeout): 10skafka_config.producer_max_buffered_bytes(-kafka.producer-max-buffered-bytes): 1 GiB (0 disables the limit)
Resolution:
-
Implement retry logic with exponential backoff in your client.
-
Check Kafka broker health and load (under-replicated partitions, broker CPU/disk/network saturation, ongoing rollouts).
-
Increase
kafka_config.write_timeoutif brokers are healthy but consistently slow to acknowledge writes:kafka_config: write_timeout: 20sEnsure that this remains less than the distributor's
-distributor.write-timeout(default: 30s) to avoid distributor-level timeouts.
Properties:
- Enforced by: Distributor (Kafka producer)
- Retryable: Yes
- HTTP status: 500 Internal Server Error
- Configurable per tenant: No
Error: Service unavailable
Error message:
Cause:
Loki is temporarily unable to handle requests due to high load or maintenance. This can occur when ingesters are unhealthy or the ring is not ready.
Default configuration:
- Ingester ring readiness: Requires minimum healthy ingesters based on replication factor
- Distributor: Returns 503 when no healthy ingesters available
Resolution:
- Implement retry logic with exponential backoff in your client.
- Scale Loki ingester instances to handle load.
- Check for resource constraints (CPU, memory, storage).
- Review ingestion patterns for sudden spikes.
Properties:
- Enforced by: Distributor/Gateway
- Retryable: Yes
- HTTP status: 503 Service Unavailable
- Configurable per tenant: No
Structured metadata errors
These errors occur when using structured metadata incorrectly.
Error: Structured metadata disabled
Error message:
stream <stream_labels> includes structured metadata, but this feature is disallowed. Please see limits_config.allow_structured_metadata or contact your Loki administrator to enable it
Cause:
allow_structured_metadata defaults to true, so this error usually means it has been explicitly disabled, either globally or as a per-tenant override.
Default configuration:
allow_structured_metadata: true (enabled by default in recent versions)
Resolution:
-
Enable structured metadata:
limits_config: allow_structured_metadata: true -
Move metadata to regular labels if structured metadata isn't needed.
-
Contact your Loki administrator to enable the feature.
Properties:
- Enforced by: Distributor
- Retryable: No
- HTTP status: 400 Bad Request
- Configurable per tenant: Yes
Error: Structured metadata too large
Error message:
stream '{job="app"}' has structured metadata too large: '70000' bytes, limit: '65536' bytes. Please see limits_config.max_structured_metadata_size or contact your Loki administrator to increase it
Cause:
The structured metadata size exceeds the configured limit.
Default configuration:
max_structured_metadata_size: 64 KB
Resolution:
-
Reduce the size of structured metadata by removing unnecessary fields.
-
Increase the limit:
limits_config: max_structured_metadata_size: 128KB -
Use compression or abbreviations for metadata values.
Properties:
- Enforced by: Distributor
- Retryable: No
- HTTP status: 400 Bad Request
- Configurable per tenant: Yes
Error: Too many structured metadata entries
Error message:
stream '{job="app"}' has too many structured metadata labels: '150', limit: '128'. Please see limits_config.max_structured_metadata_entries_count or contact your Loki administrator to increase it
Cause:
The number of structured metadata entries exceeds the limit.
Default configuration:
max_structured_metadata_entries_count: 128
Resolution:
-
Reduce the number of structured metadata entries by consolidating fields.
-
Increase the limit:
limits_config: max_structured_metadata_entries_count: 256 -
Combine related metadata into single entries using JSON or other formats.
Properties:
- Enforced by: Distributor
- Retryable: No
- HTTP status: 400 Bad Request
- Configurable per tenant: Yes
Blocked ingestion errors
These errors occur when ingestion is administratively blocked.
Ingestion blocked
Error message:
ingestion blocked for user <tenant> until <time> with status code 260
Or for policy-specific blocks:
ingestion blocked for user <tenant> (policy: <policy>) until <time> with status code 260
Cause:
Ingestion has been administratively blocked for the tenant, typically due to policy violations, billing issues, or maintenance. Status code 260 is a custom Loki status code for blocked ingestion.
Default configuration:
blocked_ingestion_status_code: 260 (custom status code)- Blocks are configured per tenant or per policy in runtime overrides
Resolution:
- Wait until the block expires if a time limit is set.
- Contact your Loki administrator to understand the block reason.
- Review tenant usage patterns and policies.
- Check for policy-specific blocks if using ingestion policies.
Properties:
- Enforced by: Distributor
- Retryable: No (until block is lifted)
- HTTP status: 260 (custom) or configured status code
- Configurable per tenant: Yes
Troubleshooting workflow
Follow this workflow when investigating ingestion issues:
-
Check metrics:
# See which errors are occurring sum by (reason) (rate(loki_discarded_samples_total[5m])) # Identify affected tenants sum by (tenant, reason) (rate(loki_discarded_bytes_total[5m])) -
Review logs from distributors and ingesters:
# Enable write failure logging per tenant limits_config: limited_log_push_errors: true -
Test push endpoint:
curl -H "Content-Type: application/json" \ -XPOST http://loki:3100/loki/api/v1/push \ --data-raw '{"streams":[{"stream":{"job":"test"},"values":[["'"$(date +%s)000000000"'","test log line"]]}]}' -
Check tenant limits:
curl http://loki:3100/loki/api/v1/user_stats -
Review configuration for affected tenant or global limits.
-
Monitor system resources:
- Ingester memory usage
- Disk space for WAL
- Storage backend health
- Network connectivity
Additional resources
- Loki Configuration Reference
- Request Validation and Rate Limits
- Write-Ahead Log Documentation
- Alloy Configuration
- Alloy loki.source.file
- Alloy loki.write
- Multi-tenancy and Limits
Troubleshoot Loki operations
This guide helps you troubleshoot errors that occur during Loki operations, including configuration issues, storage backend problems, cluster communication failures, and service component errors. These errors are distinct from ingestion (write path) and query (read path) errors covered in separate troubleshooting topics.
Before you begin, ensure you have the following:
- Access to Loki logs and metrics
- Permissions to view and modify Loki configuration
- Understanding of your deployment topology (single binary/monolithic, simple scalable, microservices/distributed)
Configuration errors
Configuration errors occur during Loki startup or when loading runtime configuration. These errors prevent Loki from starting or operating correctly.
Error: Multiple config errors found
Error message:
MULTIPLE CONFIG ERRORS FOUND, PLEASE READ CAREFULLY
<list of configuration errors>
Cause:
Multiple configuration validation errors were detected during startup. Loki aggregates all configuration errors rather than failing on the first one.
Resolution:
-
Review all listed errors carefully - each error message describes a specific configuration problem.
-
Check your configuration file for syntax errors and invalid values.
-
Validate your configuration before applying:
loki -config.file=/path/to/config.yaml -verify-config
Properties:
- Enforced by: Loki startup
- Retryable: No (configuration must be fixed)
- HTTP status: N/A (startup failure)
- Configurable per tenant: No
Error: Too many storage configs
Error message:
too many storage configs provided in the common config, please only define one storage backend
Cause:
Multiple storage backends are configured in the common configuration section. Loki requires a single storage backend for the common config.
Resolution:
-
Use only one storage backend in your common config:
common: storage: # Choose only ONE of the following: s3: endpoint: s3.amazonaws.com bucketnames: loki-data # OR gcs: bucket_name: loki-data # OR azure: container_name: loki-data -
For multiple storage backends, configure them explicitly in specific sections rather than common config.
Properties:
- Enforced by: Configuration validation
- Retryable: No
- HTTP status: N/A (startup failure)
- Configurable per tenant: No
Error: Persist tokens path prefix required
Error message:
if persist_tokens is true, path_prefix MUST be defined
Cause:
The persist_tokens option is enabled for a ring but no path_prefix is specified. Loki needs a path to store the token file.
Resolution:
-
Set the path prefix:
common: path_prefix: /var/loki persist_tokens: true ingester: lifecycler: ring: kvstore: store: memberlist tokens_file_path: /var/loki/tokens -
Or disable persist_tokens if you don't need token persistence:
common: persist_tokens: false
Properties:
- Enforced by: Configuration validation
- Retryable: No
- HTTP status: N/A (startup failure)
- Configurable per tenant: No
Error: Conflicting gRPC client configs
Error message:
both `grpc_client_config` and (`query_frontend_grpc_client` or `query_scheduler_grpc_client`) are set at the same time. Please use only `query_frontend_grpc_client` and `query_scheduler_grpc_client`
Cause:
Both the deprecated grpc_client_config and the newer specific gRPC client configs are set. These are mutually exclusive.
Resolution:
-
Remove the deprecated config and use specific gRPC client configs:
# Remove this: # grpc_client_config: ... # Use these instead: query_frontend_grpc_client: max_recv_msg_size: 104857600 query_scheduler_grpc_client: max_recv_msg_size: 104857600
Properties:
- Enforced by: Configuration validation
- Retryable: No
- HTTP status: N/A (startup failure)
- Configurable per tenant: No
Error: Schema v13 required for structured metadata
Error message:
CONFIG ERROR: schema v13 is required to store Structured Metadata and use native OTLP ingestion, your schema version is <version>. Set `allow_structured_metadata: false` in the `limits_config` section or set the command line argument `-validation.allow-structured-metadata=false` and restart Loki. Then proceed to update to schema v13 or newer before re-enabling this config, search for 'Storage Schema' in the docs for the schema update procedure
Cause:
Structured metadata is enabled but the active schema version is older than v13. Structured metadata requires schema v13 or newer.
Resolution:
-
Disable structured metadata temporarily:
limits_config: allow_structured_metadata: false -
Update your schema config to v13 or newer:
schema_config: configs: - from: "2024-04-01" store: tsdb object_store: s3 schema: v13 index: prefix: index_ period: 24h -
Re-enable structured metadata after the schema migration is complete.
Properties:
- Enforced by: Configuration validation
- Retryable: No (configuration must be fixed)
- HTTP status: N/A (startup failure)
- Configurable per tenant: No
Error: TSDB index type required for structured metadata
Error message:
CONFIG ERROR: `tsdb` index type is required to store Structured Metadata and use native OTLP ingestion, your index type is `<type>` (defined in the `store` parameter of the schema_config). Set `allow_structured_metadata: false` in the `limits_config` section or set the command line argument `-validation.allow-structured-metadata=false` and restart Loki. Then proceed to update the schema to use index type `tsdb` before re-enabling this config, search for 'Storage Schema' in the docs for the schema update procedure
Cause:
Structured metadata is enabled but the active index type is not TSDB. Structured metadata requires the TSDB index type.
Resolution:
-
Disable structured metadata temporarily and migrate to the TSDB index type:
limits_config: allow_structured_metadata: false schema_config: configs: - from: "2024-01-01" store: tsdb object_store: s3 schema: v13 -
Re-enable structured metadata after migrating to TSDB.
Properties:
- Enforced by: Configuration validation
- Retryable: No (configuration must be fixed)
- HTTP status: N/A (startup failure)
- Configurable per tenant: No
Error: TSDB directories not configured
Error message:
CONFIG ERROR: `tsdb` index type is configured in at least one schema period, however, `storage_config`, `tsdb_shipper`, `active_index_directory` is not set, please set this directly or set `path_prefix:` in the `common:` section
Or:
CONFIG ERROR: `tsdb` index type is configured in at least one schema period, however, `storage_config`, `tsdb_shipper`, `cache_location` is not set, please set this directly or set `path_prefix:` in the `common:` section
Cause:
The TSDB index type is configured in the schema but required local directories for index files are not set.
Resolution:
-
Set the common path prefix (simplest approach):
common: path_prefix: /var/loki -
Or configure directories explicitly:
storage_config: tsdb_shipper: active_index_directory: /var/loki/tsdb-index cache_location: /var/loki/tsdb-cache
Properties:
- Enforced by: Configuration validation
- Retryable: No (configuration must be fixed)
- HTTP status: N/A (startup failure)
- Configurable per tenant: No
Error: Compactor working directory empty
Error message:
CONFIG ERROR: `compactor:` `working_directory:` is empty, please set a valid directory or set `path_prefix:` in the `common:` section
Cause:
The compactor requires a working directory for index compaction, but none is configured.
Resolution:
-
Set the common path prefix:
common: path_prefix: /var/loki -
Or set the compactor working directory explicitly:
compactor: working_directory: /var/loki/compactor
Properties:
- Enforced by: Configuration validation
- Retryable: No (configuration must be fixed)
- HTTP status: N/A (startup failure)
- Configurable per tenant: No
Error: Unrecognized index or store type
Error message:
unrecognized `store` (index) type `<type>`, choose one of: <supported_types>
Or:
unrecognized `object_store` type `<type>`, which also does not match any named_stores. Choose one of: <supported_types>. Or choose a named_store
Cause:
The schema configuration references an index type or object store type that Loki does not recognize.
Resolution:
-
Use a supported index type:
tsdb -
Use a supported object store type:
s3,gcs,azure,swift,filesystem,bos -
Or reference a valid named store defined in your configuration:
storage_config: named_stores: aws: my-store: endpoint: s3.amazonaws.com bucketnames: my-bucket schema_config: configs: - from: 2024-01-01 store: tsdb object_store: my-store # References the named store
Properties:
- Enforced by: Configuration validation
- Retryable: No (configuration must be fixed)
- HTTP status: N/A (startup failure)
- Configurable per tenant: No
Error: Overrides exporter requires runtime configuration
Error message:
overrides-exporter has been enabled, but no runtime configuration file was configured
Cause:
The overrides-exporter target is enabled but no runtime configuration file is provided. The overrides-exporter needs a runtime config to expose tenant-specific limit overrides as metrics.
Resolution:
-
Configure a runtime configuration file:
runtime_config: file: /etc/loki/runtime-config.yaml -
Or disable the overrides-exporter if not needed by removing it from your target list.
Properties:
- Enforced by: Module initialization
- Retryable: No (configuration must be fixed)
- HTTP status: N/A (startup failure)
- Configurable per tenant: No
Error: Invalid override for tenant
Error message:
invalid override for tenant <tenant>: <details>
Cause:
The runtime configuration file contains an invalid override for a specific tenant. The override failed validation.
Resolution:
- Review the runtime config file for the specified tenant.
- Validate the override values against the limits configuration schema.
- Fix invalid values such as negative durations, invalid label matchers, or out-of-range settings.
Properties:
- Enforced by: Runtime configuration loader
- Retryable: No (runtime config must be fixed)
- HTTP status: N/A (runtime config reload failure)
- Configurable per tenant: Yes
Error: Retention period too short
Error message:
retention period must be >= 24h was <duration>
Cause:
A stream-level retention rule specifies a retention period shorter than 24 hours, which is the minimum allowed.
Resolution:
-
Set retention periods to at least 24 hours:
limits_config: retention_stream: - selector: '{namespace="dev"}' priority: 1 period: 24h # Must be >= 24h
Properties:
- Enforced by: Configuration validation
- Retryable: No (configuration must be fixed)
- HTTP status: N/A (startup failure)
- Configurable per tenant: Yes
Error: Invalid query store max look back period
Error message:
it is an error to specify a non zero `query_store_max_look_back_period` value when using any object store other than `filesystem`
Cause:
The query_store_max_look_back_period is set to a non-zero value with a storage backend other than filesystem. This setting only applies to local filesystem storage.
Resolution:
-
Remove the setting if using object storage:
# Remove or set to 0: query_store_max_look_back_period: 0 -
Or use filesystem storage if this setting is needed for local development.
Properties:
- Enforced by: Configuration validation
- Retryable: No (configuration must be fixed)
- HTTP status: N/A (startup failure)
- Configurable per tenant: No
Authentication and tenant errors
Authentication and tenant errors occur when requests are missing required tenant identification or when tenant IDs are invalid. In multi-tenant mode, every request must include a valid tenant ID.
Error: No org ID
Error message:
no org id
Cause:
A request was made to Loki without the required X-Scope-OrgID header. In multi-tenant mode, every request must identify the tenant.
Resolution:
-
Add the
X-Scope-OrgIDheader to your requests:curl -H "X-Scope-OrgID: my-tenant" http://loki:3100/loki/api/v1/push ... -
For Grafana, configure the tenant ID in the Loki data source settings under "HTTP Headers".
-
For Alloy, set the tenant ID in the
loki.writecomponent:loki.write "default" { endpoint { url = "http://loki:3100/loki/api/v1/push" tenant_id = "my-tenant" } } -
Disable multi-tenancy for single-tenant deployments:
auth_enabled: false
Properties:
- Enforced by: Authentication middleware
- Retryable: Yes (with tenant ID)
- HTTP status: 401 Unauthorized
- Configurable per tenant: No
Error: Multiple org IDs present
Error message:
multiple org IDs present
Cause:
The request contains multiple different tenant IDs, but the operation requires a single tenant. This can happen when a request is forwarded through multiple proxies that each inject a tenant ID.
Resolution:
-
Ensure only one tenant ID is set in the
X-Scope-OrgIDheader. -
Check proxy configurations for conflicting tenant ID injection.
-
For cross-tenant queries, use pipe-separated tenant IDs only where supported:
curl -H "X-Scope-OrgID: tenant1|tenant2" http://loki:3100/loki/api/v1/query ...
Properties:
- Enforced by: Tenant resolver
- Retryable: Yes (with correct tenant ID)
- HTTP status: 400 Bad Request
- Configurable per tenant: No
Error: Tenant ID too long
Error message:
tenant ID is too long: max 150 characters
Cause:
The tenant ID exceeds the maximum allowed length of 150 characters.
Resolution:
- Use a shorter tenant ID (maximum 150 characters).
Properties:
- Enforced by: Tenant validation
- Retryable: Yes (with valid tenant ID)
- HTTP status: 400 Bad Request
- Configurable per tenant: No
Error: Unsafe tenant ID
Error message:
tenant ID is '.' or '..'
Cause:
The tenant ID is set to . or .., which are reserved filesystem path components and could cause path traversal issues.
Resolution:
- Choose a different tenant ID that is not
.or...
Properties:
- Enforced by: Tenant validation
- Retryable: Yes (with valid tenant ID)
- HTTP status: 400 Bad Request
- Configurable per tenant: No
Error: Tenant ID contains unsupported character
Error message:
tenant ID '<id>' contains unsupported character '<char>'
Cause:
The tenant ID contains characters that are not allowed. Tenant IDs must consist of alphanumeric characters, hyphens, underscores, and periods.
Resolution:
- Use only supported characters in your tenant ID: letters, numbers, hyphens (
-), underscores (_), and periods (.).
Properties:
- Enforced by: Tenant validation
- Retryable: Yes (with valid tenant ID)
- HTTP status: 400 Bad Request
- Configurable per tenant: No
Error: Deletion not available for tenant
Error message:
deletion is not available for this tenant
Cause:
A delete request was submitted for a tenant, but the deletion API endpoints aren't reachable. This happens when retention_enabled is false in the compactor configuration (its default), or when the tenant's deletion_mode override has been explicitly set to disabled.
Resolution:
-
Enable retention on the compactor, which gates the deletion API endpoints:
compactor: retention_enabled: true -
If needed, override the deletion mode for the tenant in the runtime configuration:
overrides: my-tenant: deletion_mode: filter-and-delete # Or "filter-only"Valid deletion modes:
disabled- Deletion is not allowedfilter-only- Lines matching delete requests are filtered at query time but not physically deletedfilter-and-delete- Lines are filtered at query time and physically deleted during compaction (default)
-
Ensure the compactor is configured for retention:
compactor: retention_enabled: true delete_request_store: s3
Properties:
- Enforced by: Compactor deletion handler
- Retryable: No (configuration must change)
- HTTP status: 403 Forbidden
- Configurable per tenant: Yes
Storage backend errors
Storage backend errors occur when Loki cannot communicate with or properly configure object storage (Amazon S3, Google Cloud Services, Microsoft Azure, Swift, or filesystem).
Error: Unsupported storage backend
Error message:
unsupported storage backend
Cause:
The specified storage backend type is not recognized. This typically occurs when a typo exists in the storage type configuration.
Resolution:
-
Use a valid storage backend type:
s3- Amazon S3 or S3-compatible storagegcs- Google Cloud Storageazure- Azure Blob Storageswift- OpenStack Swiftfilesystem- Local filesystembos- Baidu Object Storage
storage_config: tsdb_shipper: shared_store: s3 # Must be one of the valid types
Properties:
- Enforced by: Configuration validation
- Retryable: No
- HTTP status: N/A (startup failure)
- Configurable per tenant: No
Error: Invalid characters in storage prefix
Error message:
storage prefix contains invalid characters, it may only contain digits, English alphabet letters and dashes
Cause:
The storage path prefix contains invalid characters. Only alphanumeric characters and dashes are allowed.
Resolution:
-
Use valid characters in your storage prefix:
storage_config: # Invalid: prefix_with_underscore_or/special chars # Valid: my-loki-data or lokilogs123 aws: s3: s3://my-bucket/my-loki-data
Properties:
- Enforced by: Configuration validation
- Retryable: No
- HTTP status: N/A (startup failure)
- Configurable per tenant: No
Error: Unsupported S3 SSE type
Error message:
unsupported S3 SSE type
Cause:
The S3 server-side encryption (SSE) type is not supported. Loki supports specific SSE types.
Resolution:
-
Use a supported SSE type:
storage_config: aws: sse: type: SSE-S3 # Or SSE-KMSSupported types:
SSE-S3- Server-side encryption with Amazon S3-managed keysSSE-KMS- Server-side encryption with AWS KMS-managed keys
Properties:
- Enforced by: Configuration validation
- Retryable: No
- HTTP status: N/A (startup failure)
- Configurable per tenant: No
Error: Invalid S3 SSE encryption context
Error message:
invalid S3 SSE encryption context
Cause:
The SSE-KMS encryption context is malformed and cannot be parsed as valid JSON.
Resolution:
-
Provide valid JSON for the encryption context:
storage_config: aws: sse: type: SSE-KMS kms_key_id: alias/my-key kms_encryption_context: '{"key": "value"}' # Valid JSON
Properties:
- Enforced by: Configuration validation
- Retryable: No
- HTTP status: N/A (startup failure)
- Configurable per tenant: No
Error: Invalid S3 endpoint prefix
Error message:
the endpoint must not prefixed with the bucket name
Cause:
The S3 endpoint incorrectly includes the bucket name as a prefix. This can cause path-style vs virtual-hosted-style URL issues.
Resolution:
-
Remove the bucket name from the endpoint and configure it separately:
storage_config: aws: # Incorrect: # endpoint: my-bucket.s3.amazonaws.com # Correct: endpoint: s3.amazonaws.com bucketnames: my-bucket
Properties:
- Enforced by: Configuration validation
- Retryable: No
- HTTP status: N/A (startup failure)
- Configurable per tenant: No
Error: Invalid STS endpoint
Error message:
sts-endpoint must be a valid url
Cause:
The AWS STS (Security Token Service) endpoint URL is malformed or invalid.
Resolution:
-
Provide a valid URL for the STS endpoint:
storage_config: aws: sts_endpoint: https://sts.us-east-1.amazonaws.com
Properties:
- Enforced by: Configuration validation
- Retryable: No
- HTTP status: N/A (startup failure)
- Configurable per tenant: No
Error: Azure connection string malformed
Error message:
connection string is either blank or malformed. The expected connection string should contain key value pairs separated by semicolons. For example 'DefaultEndpointsProtocol=https;AccountName=<accountName>;AccountKey=<accountKey>;EndpointSuffix=core.windows.net'
Cause:
The Azure storage connection string is missing or doesn't follow the expected format.
Resolution:
-
Use a valid connection string format:
storage_config: azure: # Use account credentials: account_name: myaccount account_key: mykey # Or connection string: connection_string: "DefaultEndpointsProtocol=https;AccountName=myaccount;AccountKey=mykey;EndpointSuffix=core.windows.net" -
Verify the connection string in Azure Portal under Storage Account > Access Keys.
Properties:
- Enforced by: Configuration validation
- Retryable: No
- HTTP status: N/A (startup failure)
- Configurable per tenant: No
Error: Unrecognized named storage config
Error message:
unrecognized named storage config <name>
Or for specific backends:
unrecognized named s3 storage config <name>
unrecognized named gcs storage config <name>
unrecognized named azure storage config <name>
unrecognized named filesystem storage config <name>
unrecognized named swift storage config <name>
Or for an unrecognized store type:
unrecognized named storage type: <storeType>
Cause:
A named storage configuration referenced in the schema config doesn't exist in the named stores configuration.
Resolution:
-
Define the named store in your configuration:
storage_config: named_stores: aws: my-s3-store: # This name must match the reference endpoint: s3.amazonaws.com bucketnames: my-bucket schema_config: configs: - from: 2024-01-01 store: tsdb object_store: my-s3-store # References the named store above -
Check spelling of the store name in both the definition and reference.
Properties:
- Enforced by: Configuration validation
- Retryable: No
- HTTP status: N/A (startup failure)
- Configurable per tenant: No
Cache errors
Cache errors occur when Loki cannot connect to or communicate with caching backends (Memcached, Redis).
Error: Redis client setup failed
Error message:
redis client setup failed: <details>
Cause:
Loki cannot establish a connection to the Redis server. Common causes include:
- Incorrect Redis endpoint
- Network connectivity issues
- Authentication failures
- TLS configuration problems
Resolution:
-
Verify Redis connectivity from the Loki host:
redis-cli -h <REDIS-HOST> -p <REDIS-PORT> ping -
Check the Redis endpoint configuration:
chunk_store_config: chunk_cache_config: redis: endpoint: redis:6379 timeout: 500ms -
Configure authentication if required:
chunk_store_config: chunk_cache_config: redis: endpoint: redis:6379 password: ${REDIS_PASSWORD}
Properties:
- Enforced by: Cache client initialization
- Retryable: Yes (with correct configuration)
- HTTP status: N/A (startup failure or degraded operation)
- Configurable per tenant: No
Error: Could not lookup Redis host
Error message:
could not lookup host: <hostname>
Cause:
DNS resolution failed for the Redis hostname.
Resolution:
-
Verify DNS resolution:
nslookup redis-host -
Use an IP address if DNS is not available:
chunk_store_config: chunk_cache_config: redis: endpoint: 10.0.0.100:6379 -
Check your DNS configuration and network settings.
Properties:
- Enforced by: DNS resolution
- Retryable: Yes
- HTTP status: N/A
- Configurable per tenant: No
Error: Unexpected Redis PING response
Error message:
redis: Unexpected PING response "<response>"
Cause:
The Redis server returned an unexpected response to a PING command. This could indicate:
- The endpoint is not a Redis server
- A proxy or load balancer is interfering
- Redis is in an error state
Resolution:
-
Verify the endpoint is actually a Redis server.
-
Check Redis health:
redis-cli -h <HOST> -p <PORT> INFO -
Review proxy configurations if using a load balancer in front of Redis.
Properties:
- Enforced by: Redis health check
- Retryable: Yes
- HTTP status: N/A
- Configurable per tenant: No
Error: Multiple cache systems not supported
Error message:
use of multiple cache storage systems is not supported
Cause:
Both Memcached and Redis cache backends are configured for the same cache type. Only one caching backend is supported per cache type.
Resolution:
-
Choose one cache backend per cache type:
chunk_store_config: chunk_cache_config: # Use either memcached OR redis, not both redis: endpoint: redis:6379 memcached: {} # Remove this
Properties:
- Enforced by: Configuration validation
- Retryable: No
- HTTP status: N/A (startup failure)
- Configurable per tenant: No
Error: No cache configured
Error message:
no cache configured
Cause:
A results cache is required for the query frontend but no cache configuration was provided.
Resolution:
-
Configure a cache backend:
query_range: results_cache: cache: memcached: expiration: 1h memcached_client: addresses: memcached:11211 -
Or disable results caching if not needed:
query_range: cache_results: false
Properties:
- Enforced by: Query frontend initialization
- Retryable: No
- HTTP status: N/A (startup failure)
- Configurable per tenant: No
Error: Error loading cache generation numbers
Error message:
msg="error loading cache generation numbers" err="unexpected status code: 403"
Or from the compactor HTTP client:
msg="error getting cache gen numbers from the store" err="unexpected status code: 403"
Metric: loki_delete_cache_gen_load_failures_total
Cause:
Loki uses cache generation numbers to invalidate query caches when log deletion requests are processed. The cache generation number loader periodically fetches these numbers from the compactor. When the compactor returns HTTP 403, it means the deletion API is not enabled for the tenant. The loader logs this error and increments the loki_delete_cache_gen_load_failures_total metric.
Other non-403 causes include:
- The compactor is unreachable (network or DNS issues)
- The compactor is not running or not ready
- TLS or authentication misconfiguration between the querier and compactor
Resolution:
-
Check if deletion is intentionally disabled. If you don't use log deletion for this tenant, these errors are harmless but noisy. You can verify by sending a GET request to the compactor's cache generation number endpoint:
curl -s -H "X-Scope-OrgID: <tenant>" http://compactor:3100/loki/api/v1/cache/generation_numbersIf the response is
"deletion is not available for this tenant", the deletion API is not enabled for the tenant. -
Enable deletion for the tenant if log deletion is required:
overrides: <tenant>: deletion_mode: filter-and-delete -
Check compactor connectivity if the error includes a non-403 status code or a connection error:
curl -s http://compactor:3100/ready -
Verify the compactor address is correctly configured. Queriers and other components that use the cache generation loader need to reach the compactor:
compactor: compactor_address: http://compactor:3100
Properties:
- Enforced by: Cache generation number loader (
GenNumberLoader) - Retryable: Yes (the loader retries automatically every 5 minutes)
- HTTP status: N/A (background process, not a request-time error)
- Configurable per tenant: Yes (via
deletion_modein tenant overrides)
Ring and cluster communication errors
Ring errors occur when Loki components cannot properly communicate through the hash ring, which is used to distribute work across instances. The ring is fundamental to Loki's distributed operation.
Error: Too many unhealthy instances in the ring
Error message:
too many unhealthy instances in the ring
Cause:
The ring contains too many unhealthy instances to satisfy the replication factor. For example, with a replication factor of 3, at least 3 healthy instances must be available.
Resolution:
-
Check the health of ring members:
curl -s http://loki:3100/ring | jq '.shards[] | select(.state != "ACTIVE")' -
Restart unhealthy instances that are stuck in a bad state.
-
Scale up instances if there aren't enough healthy members.
-
Check resource constraints (CPU, memory, disk) on unhealthy instances.
Properties:
- Enforced by: Ring replication
- Retryable: Yes (after instances recover)
- HTTP status: 503 Service Unavailable
- Configurable per tenant: No
Error: Empty ring
Error message:
empty ring
Cause:
No instances are registered in the ring. This typically occurs during initial cluster startup, for example if your ingesters are OOM crashing, or due to misconfiguration.
Resolution:
-
Wait for instances to register during initial startup.
-
Check ingesters to make sure they are running.
-
Check that all instances can communicate over the configured ports.
-
Verify ring configuration across all components, especially memberlist configuration:
ingester: lifecycler: ring: kvstore: store: memberlist replication_factor: 3 -
Check KV store health (Consul, etcd, or memberlist):
# For memberlist curl -s http://loki:3100/memberlist
Properties:
- Enforced by: Ring operations
- Retryable: Yes (after instances register)
- HTTP status: 503 Service Unavailable
- Configurable per tenant: No
Error: Instance not found in the ring
Error message:
instance <id> not found in the ring
Cause:
A specific instance is expected to be in the ring but isn't registered. This can happen after a restart if the instance hasn't re-joined the ring yet.
Resolution:
- Wait for the instance to re-register in the ring.
- Check the instance's logs for ring join failures.
- Verify KV store connectivity from the instance.
Properties:
- Enforced by: Ring operations
- Retryable: Yes (after instance registers)
- HTTP status: 503 Service Unavailable
- Configurable per tenant: No
Error: Instance owns no tokens
Error message:
this instance owns no tokens
Cause:
The instance has joined the ring but hasn't claimed any tokens. Without tokens, the instance cannot receive any work. This can happen if:
- The instance is still starting up
- Token claim failed
- The KV store update didn't propagate
Resolution:
-
Wait for token assignment during startup.
-
Check the ring status for the instance: Open a browser and navigate to http://localhost:3100/ring. You should see the Loki Ring Status page.
OR
curl -s http://loki:3100/ring -
Restart the instance if tokens are not assigned after startup completes.
-
Check KV store connectivity and health.
Properties:
- Enforced by: Lifecycler readiness check
- Retryable: Yes (after tokens are assigned)
- HTTP status: 503 Service Unavailable
- Configurable per tenant: No
Error: Error talking to the KV store
Error message:
error talking to the KV store
Cause:
The instance cannot communicate with the key-value store used for ring state. The KV store (Consul, etcd, or memberlist) is required for ring coordination.
Resolution:
-
Check KV store health and connectivity:
# For Consul curl http://consul:8500/v1/status/leader # For etcd etcdctl endpoint health -
Verify network connectivity between Loki instances and the KV store.
-
Check firewall rules allow traffic on KV store ports.
-
For memberlist, verify that gossip ports are accessible between all instances:
memberlist: bind_port: 7946 join_members: - loki-memberlist:7946
Properties:
- Enforced by: Ring lifecycler
- Retryable: Yes (after KV store recovery)
- HTTP status: 503 Service Unavailable
- Configurable per tenant: No
Error: No ring returned from the KV store
Error message:
no ring returned from the KV store
Cause:
The KV store responded but returned an empty or invalid ring descriptor. This can happen if the KV store was recently initialized or its data was cleared.
Resolution:
- Wait for ring initialization during first startup.
- Check if the KV store data was accidentally cleared.
- Restart all ring members to re-register if the KV store was reset.
Properties:
- Enforced by: Ring lifecycler
- Retryable: Yes (after ring initialization)
- HTTP status: 503 Service Unavailable
- Configurable per tenant: No
Error: Failed to join memberlist cluster
Error message:
failed to join memberlist cluster on startup
Or:
joining memberlist cluster failed
Cause:
The instance could not join the memberlist gossip cluster. Common causes:
- Seed nodes are unreachable
- DNS resolution failure for join addresses
- Firewall blocking gossip ports
- All existing members are down
Resolution:
-
Check that join members are reachable:
# Test connectivity to seed nodes nc -zv loki-memberlist 7946 -
Verify DNS resolution for join addresses:
nslookup loki-memberlist -
Check memberlist configuration:
memberlist: bind_port: 7946 join_members: - loki-gossip-ring.loki.svc.cluster.local:7946 -
Ensure firewall rules allow UDP and TCP traffic on the gossip port (default 7946).
-
For Kubernetes, verify that the headless service for memberlist is configured correctly.
Properties:
- Enforced by: Memberlist KV client
- Retryable: Yes (automatic retries with backoff)
- HTTP status: N/A (startup failure or degraded operation)
- Configurable per tenant: No
Error: Re-joining memberlist cluster failed
Error message:
re-joining memberlist cluster failed
Cause:
After being disconnected from the memberlist cluster, the instance failed to rejoin. This can happen during network partitions or after prolonged network issues.
Resolution:
- Check network connectivity between cluster members.
- Verify other cluster members are healthy.
- Restart the affected instance if automatic rejoin continues to fail.
- Review network stability frequent re-joins indicate underlying network issues.
Properties:
- Enforced by: Memberlist KV client
- Retryable: Yes (automatic retries)
- HTTP status: N/A (degraded operation)
- Configurable per tenant: No
Component readiness errors
Readiness errors occur when Loki components are not ready to serve requests. These errors are returned by the /ready health check endpoint and prevent load balancers from routing traffic to unready instances.
Error: Application is stopping
Error message:
Application is stopping
Cause:
Loki is shutting down and no longer accepting new requests. This is normal during graceful shutdown.
Resolution:
- Wait for the instance to restart if this is a rolling update.
- Check if the shutdown is expected (maintenance, scaling down).
- Review orchestrator logs (Kubernetes, systemd) if the shutdown is unexpected.
Properties:
- Enforced by: Loki readiness handler
- Retryable: Yes (after restart)
- HTTP status: 503 Service Unavailable
- Configurable per tenant: No
Error: Some services are not running
Error message:
Some services are not Running:
<state>: <count>
<state>: <count>
For example:
Some services are not Running:
Starting: 1
Failed: 2
Cause:
One or more internal Loki services have failed to start or have stopped unexpectedly. The error message lists each service state with a count of services in that state.
Resolution:
- Check Loki logs for errors from the listed services.
- Verify configuration for the affected services.
- Check resource availability (memory, disk, CPU).
- Restart the instance if services are stuck.
Properties:
- Enforced by: Loki service manager
- Retryable: Yes (after services recover)
- HTTP status: 503 Service Unavailable
- Configurable per tenant: No
Error: Ingester not ready
Error message:
Ingester not ready: <details>
When the ingester's own state check fails, <details> contains the ingester state, giving the full message:
Ingester not ready: ingester not ready: <state>
Where <state> is the service state, for example Starting, Stopping, or Failed.
Cause:
The ingester is not in a ready state to accept writes or serve reads. The detail message indicates the specific reason, such as:
- The ingester is still starting up and joining the ring (
Starting) - The lifecycler is not ready (lifecycler error text)
- The ingester is waiting for minimum ready duration after ring join
Resolution:
-
Wait for startup to complete - ingesters take time to join the ring and become ready.
-
Check ring membership: Open a browser and navigate to http://localhost:3100/ring. You should see the Loki Ring Status page.
OR
curl -s http://ingester:3100/ring -
Review logs for startup errors.
-
Adjust the minimum ready duration if startup is too slow:
ingester: lifecycler: min_ready_duration: 15s
Properties:
- Enforced by: Ingester readiness check
- Retryable: Yes (after ingester becomes ready)
- HTTP status: 503 Service Unavailable
- Configurable per tenant: No
Error: No queriers connected to query frontend
Error message:
Query Frontend not ready: not ready: number of queriers connected to query-frontend is 0
Cause:
The query frontend has no querier workers connected. Without queriers, the frontend cannot process any queries. This typically occurs when:
- Queriers are not yet started
- Queriers cannot reach the frontend
- gRPC connectivity issues between queriers and frontend
Resolution:
-
Check that queriers are running and healthy.
-
Verify querier configuration points to the correct frontend address:
frontend_worker: frontend_address: query-frontend:9095 -
Check gRPC connectivity between queriers and the frontend:
# Test gRPC port connectivity nc -zv query-frontend 9095 -
Review querier logs for connection errors.
Properties:
- Enforced by: Query frontend (v1) readiness check
- Retryable: Yes (after queriers connect)
- HTTP status: 503 Service Unavailable
- Configurable per tenant: No
Error: No schedulers connected to frontend worker
Error message:
Query Frontend not ready: not ready: number of schedulers this worker is connected to is 0
Cause:
The query frontend worker has no active connections to any query scheduler. This prevents the frontend from dispatching queries.
Resolution:
-
Check that query schedulers are running and healthy.
-
Verify scheduler address configuration:
frontend_worker: scheduler_address: query-scheduler:9095 -
Check gRPC connectivity between the frontend and schedulers.
-
Review query scheduler logs for errors.
Properties:
- Enforced by: Query frontend (v2) readiness check
- Retryable: Yes (after schedulers connect)
- HTTP status: 503 Service Unavailable
- Configurable per tenant: No
gRPC and message size errors
gRPC errors occur during inter-component communication. Loki components communicate using gRPC for ring coordination, query execution, and data transfer.
Error: Message size too large
Error message:
message size too large than max (<size> vs <max>)
Or for the decompressed body:
decompressed message size too large than max (<size> vs <max>)
Cause:
The compressed or decompressed body of an HTTP push request to the distributor exceeds the configured limit.
Default configuration:
distributor.max_recv_msg_size: 100MB (compressed request body limit)distributor.max_decompressed_size: 5000MB (decompressed body limit, defaults to 50×max_recv_msg_size)
Resolution:
-
Increase the distributor receive message size limit:
distributor: max_recv_msg_size: 209715200 # 200MB compressed max_decompressed_size: 10737418240 # 10GB decompressed -
Reduce push batch sizes in your log shipping client (Alloy, etc.) to send smaller individual requests.
-
Reduce the amount of data per request by lowering the batch size or flush interval in your client.
Properties:
- Enforced by: Distributor push handler
- Retryable: No (request must be smaller or limits increased)
- HTTP status: 413 Request Entity Too Large (compressed), 400 Bad Request (decompressed)
- Configurable per tenant: No
Error: Response larger than max message size
Error message:
response larger than the max message size (<size> vs <max>)
Cause:
A query result from the querier to the frontend exceeds the maximum allowed gRPC response size. This typically happens with queries that return very large result sets.
Default configuration:
server.grpc_server_max_send_msg_size: 4MB (gRPC server send limit on the querier)querier.query_frontend_grpc_client.max_recv_msg_size: 100MB (gRPC client receive limit on the querier worker)
Resolution:
-
Reduce query scope to return fewer results:
- Add more specific label matchers
- Reduce the time range
- Lower the entries limit
-
Increase gRPC message size limits if needed. Apply these settings to querier nodes:
server: grpc_server_max_send_msg_size: 209715200 # 200MB querier: query_frontend_grpc_client: max_recv_msg_size: 209715200 # 200MB
Properties:
- Enforced by: Querier worker
- Retryable: No (query scope or limits must change)
- HTTP status: 413 Request Entity Too Large
- Configurable per tenant: No
Error: Compressed message size exceeds limit
Error message:
compressed message size <size> exceeds limit <limit>
Cause:
The compressed body of an HTTP push request exceeds the distributor's configured limit. This check runs after the request body has been fully read and validates the total compressed size against the configured maximum.
Default configuration:
distributor.max_recv_msg_size: 100MB
Resolution:
-
Reduce batch sizes in your log shipping client.
-
Split large batches into smaller, more frequent requests.
-
Increase the limit if needed:
distributor: max_recv_msg_size: 209715200 # 200MB
Properties:
- Enforced by: Distributor push handler
- Retryable: No (request must be smaller)
- HTTP status: 400 Bad Request
- Configurable per tenant: No
TLS and certificate errors
TLS errors occur when Loki or its clients cannot establish secure connections due to certificate issues.
Error: TLS certificate loading failed
Error message:
error loading ca cert: <path>
Or:
error loading client cert: <path>
Or:
error loading client key: <path>
Or:
failed to load TLS certificate <cert_path>,<key_path>
Cause:
Loki cannot load TLS certificates from the specified paths. Common causes:
- Certificate files don't exist at the configured paths
- Permission issues prevent reading the files
- Certificate or key format is invalid
- Certificate and key don't match
Resolution:
-
Verify certificate files exist and are readable:
ls -la /path/to/cert.pem /path/to/key.pem /path/to/ca.pem -
Check file permissions (the Loki process must be able to read them).
-
Validate the certificate format:
openssl x509 -in /path/to/cert.pem -noout -text openssl rsa -in /path/to/key.pem -check -
Verify cert and key match:
openssl x509 -noout -modulus -in cert.pem | md5sum openssl rsa -noout -modulus -in key.pem | md5sum # Both should produce the same hash -
Check your TLS configuration:
server: http_tls_config: cert_file: /path/to/cert.pem key_file: /path/to/key.pem client_ca_file: /path/to/ca.pem grpc_tls_config: cert_file: /path/to/cert.pem key_file: /path/to/key.pem client_ca_file: /path/to/ca.pem
Properties:
- Enforced by: TLS configuration
- Retryable: No (certificates must be fixed)
- HTTP status: N/A (startup failure)
- Configurable per tenant: No
Error: TLS configuration error
Error message:
error generating http tls config: <details>
Or:
error generating grpc tls config: <details>
Where <details> may include messages such as TLS version %q not recognized, cipher suite %q not recognized, or unknown TLS version: <version>.
Cause:
The TLS configuration is invalid. This can happen when:
- An unsupported TLS version string is supplied
- Cipher suite configuration is invalid
- Client auth type is unrecognized
Resolution:
-
Review TLS settings for compatibility issues.
-
Use supported TLS versions by setting
tls_min_versionat the top level of theserverblock:server: tls_min_version: VersionTLS12Valid values are
VersionTLS10,VersionTLS11,VersionTLS12, andVersionTLS13. There is nomax_versionsetting;tls_min_versionis the only version constraint. -
Check cipher suite configuration if customized.
Properties:
- Enforced by: TLS initialization
- Retryable: No (configuration must be fixed)
- HTTP status: N/A (startup failure)
- Configurable per tenant: No
DNS resolution errors
DNS errors occur when Loki cannot resolve hostnames for service discovery or backend connections.
Error: DNS lookup timeout
Error message:
msg="failed to resolve server addresses" err="... DNS lookup timeout: [<address>] ..."
Cause:
DNS resolution exceeded the 5-second timeout when trying to resolve addresses for Loki service discovery or backend connections.
This error is emitted by the index gateway and bloom gateway DNS discovery loops.
The DNS lookup timeout: [<address>] string is the context cause embedded within the err field; the full address list is formatted as a Go slice (for example, [dns+loki-index-gateway.loki.svc.cluster.local:9095]).
Resolution:
-
Check DNS server availability and configuration.
-
Verify hostname resolution:
nslookup <hostname> dig <hostname> -
Use IP addresses as a workaround if DNS is unreliable:
# Instead of dns+hostname:port memberlist: join_members: - 10.0.0.1:7946 - 10.0.0.2:7946 -
For Kubernetes, ensure CoreDNS is healthy and headless services are configured correctly.
Properties:
- Enforced by: Index gateway client, bloom gateway client DNS discovery loop
- Retryable: Yes (DNS may recover)
- HTTP status: N/A (connectivity failure)
- Configurable per tenant: No
Scheduler and frontend errors
These errors relate to query scheduling, frontend workers, and queue management.
Error: Scheduler is not running
Error message:
scheduler is not running
Cause:
The query scheduler service is not in a running state. This can occur when:
- The scheduler is starting up
- The scheduler encountered a fatal error
- The scheduler is shutting down
Resolution:
-
Check scheduler logs for startup errors or crashes.
-
Verify scheduler health:
curl -s http://scheduler:3100/ready -
Check scheduler ring membership if using ring-based scheduling:
curl -s http://scheduler:3100/ring | jq
Properties:
- Enforced by: Scheduler service
- Retryable: Yes (wait for scheduler to become ready)
- HTTP status: 503 Service Unavailable
- Configurable per tenant: No
Error: Too many outstanding requests
Error message:
too many outstanding requests
Cause:
The query queue has reached its maximum capacity. This indicates the system is overloaded with queries.
Resolution:
-
Scale out queriers to process queries faster:
querier: max_concurrent: 10 -
Increase queue capacity (with caution). The default is
32000; increase beyond that only if you have confirmed the system can handle the additional load. Note that increasing the queue is often necessary because of how many subqueries can be generated by large values fortsdb_max_query_parallelism. Generally it's preferable to add more queriers and leave this setting unchanged.query_scheduler: max_outstanding_requests_per_tenant: 64000 -
Rate limit queries at the client or load balancer level.
-
Optimize slow queries to reduce queue time.
Properties:
- Enforced by: Query scheduler/frontend
- Retryable: Yes
- HTTP status: 429 Too Many Requests
- Configurable per tenant: No
Error: Querying is disabled
Error message:
querying is disabled, please contact your Loki operator
Cause:
Query parallelism has been set to zero, effectively disabling all queries. This is typically done intentionally during maintenance.
Resolution:
-
Check the relevant parallelism setting for your index type. For TSDB indexes (the current default),
tsdb_max_query_parallelismsupersedesmax_query_parallelism. Either value being set to zero triggers this error. Verify that both are greater than zero:limits_config: max_query_parallelism: 32 # default; applies to non-TSDB schemas tsdb_max_query_parallelism: 128 # default; applies to TSDB schemas -
Size
tsdb_max_query_parallelismto your ingest volume. Typical values in production are in the range of 128–2048, proportional to the volume of logs ingested per day:Daily ingest volume Typical value Low–moderate 128–256 High 512 Tens of TB/day 1024–2048 -
Account for the querier capacity this requires. Each unit of parallelism consumes one querier worker slot. With the default
querier.max_concurrentof4, the number of queriers needed to fully parallelize a single query is:queriers needed = tsdb_max_query_parallelism / max_concurrentFor example,
tsdb_max_query_parallelism: 2048withmax_concurrent: 4requires 512 queriers to run one query fully in parallel. Production deployments supporting many tenants running large queries simultaneously commonly run thousands of queriers. -
Contact your administrator if you don't have access to change these settings.
Properties:
- Enforced by: Query frontend
- Retryable: No (configuration must change)
- HTTP status: 429 Too Many Requests
- Configurable per tenant: Yes
Error: No frontend address
Error message:
no frontend address
Cause:
The scheduler received a request from a frontend but no frontend address was provided for sending responses back.
Resolution:
-
Check frontend configuration to ensure the address is set:
frontend: address: query-frontend:9095 -
Verify gRPC connectivity between frontend and scheduler.
Properties:
- Enforced by: Scheduler
- Retryable: No (configuration issue)
- HTTP status: 400 Bad Request
- Configurable per tenant: No
Error: Scheduler shutting down
Error message:
scheduler is shutting down
Cause:
The frontend scheduler worker detected that the scheduler is in shutdown mode and cannot accept new requests.
Resolution:
- Wait for shutdown to complete and the scheduler to restart.
- Check if this is expected (rolling update, maintenance).
- Retry the request after the scheduler is healthy.
Properties:
- Enforced by: Scheduler
- Retryable: Yes (after scheduler restart)
- HTTP status: 503 Service Unavailable
- Configurable per tenant: No
Index gateway errors
Index gateway errors occur when queriers cannot communicate with index gateways for index lookups.
Error: Index gateway unhealthy in ring
Error message:
index-gateway is unhealthy in the ring
Cause:
The index gateway instance detects itself as unhealthy in the ring and refuses to process queries. This is a self-check: before handling tenant requests, the gateway verifies it appears in the set of healthy ring members.
Resolution:
-
Check index gateway health:
curl -s http://index-gateway:3100/ready -
View the ring status: Open a browser and navigate to http://localhost:3100/ring. You should see the Loki Ring Status page.
OR
curl -s http://index-gateway:3100/ring -
Check logs for errors preventing the gateway from becoming healthy.
-
Restart the index gateway if it's stuck in an unhealthy state.
Properties:
- Enforced by: Index gateway ring
- Retryable: Yes (wait for gateway to become healthy)
- HTTP status: 500 Internal Server Error
- Configurable per tenant: No
Error: No index gateway instances found
Error message:
no index gateway instances found for tenant <tenant>
Cause:
No index gateway instances are available in the ring to serve the tenant's request. This could be due to:
- All index gateways are unhealthy
- Shuffle sharding excludes this tenant
- Ring is empty
Resolution:
-
Check if any index gateways are running:
curl -s http://index-gateway:3100/ring | jq '.shards | length' -
Verify ring mode is configured if using shuffle sharding. The index gateway must run in
ringmode and the per-tenant shard size must be set:index_gateway: mode: ring limits_config: index_gateway_shard_size: 3 # default = 0 (use all instances) -
Scale up index gateways if needed.
Properties:
- Enforced by: Index gateway client
- Retryable: Yes
- HTTP status: 500 Internal Server Error
- Configurable per tenant: Yes (via
index_gateway_shard_sizeinlimits_config)
Compactor and retention errors
Compactor errors occur during index compaction or retention enforcement.
Error: No chunks found in table
Error message:
no chunks found in table, please check if there are really no chunks and manually drop the table or see if there is a bug causing us to drop whole index table
Cause:
The compactor found an empty index table during retention processing. This could indicate:
- All chunks in the table have expired
- The table was never populated
- Data corruption
Resolution:
-
Verify the table should be empty:
# Check if data exists for the time period logcli query '{job=~".+"}' --from="<table-start-time>" --to="<table-end-time>" --limit=1 -
If the table is legitimately empty, manually delete it from object storage.
-
If data should exist, investigate potential data loss.
Properties:
- Enforced by: Compactor retention
- Retryable: No (requires manual intervention)
- HTTP status: N/A (background process)
- Configurable per tenant: No
Error: Delete request store not configured
Error message:
compactor.delete-request-store should be configured when retention is enabled
Cause:
Retention is enabled but no store is configured for tracking delete requests.
Resolution:
-
Configure the delete request store:
compactor: retention_enabled: true delete_request_store: s3 -
Or disable retention if not needed:
compactor: retention_enabled: false
Properties:
- Enforced by: Configuration validation
- Retryable: No
- HTTP status: N/A (startup failure)
- Configurable per tenant: No
Error: Max compaction parallelism invalid
Error message:
max compaction parallelism must be >= 1
Cause:
The compactor's parallelism setting is configured to zero or a negative number.
Resolution:
-
Set a valid parallelism value:
compactor: max_compaction_parallelism: 1 # Must be >= 1
Properties:
- Enforced by: Configuration validation
- Retryable: No
- HTTP status: N/A (startup failure)
- Configurable per tenant: No
Error: Delete request not found
Error message:
could not find delete request with given id
Cause:
An attempt to cancel a delete request failed because no matching request exists.
Resolution:
-
List existing delete requests:
curl -s http://compactor:3100/loki/api/v1/delete | jq -
Verify the delete request ID is correct.
-
Check if the request has already been processed and removed.
Properties:
- Enforced by: Compactor API
- Retryable: No
- HTTP status: 404 Not Found
- Configurable per tenant: No
Error: Retention is not enabled
Error message:
Retention is not enabled
Cause:
A delete request was submitted but retention is not enabled in the compactor configuration. Delete requests require retention to be enabled.
Resolution:
-
Enable retention in the compactor:
compactor: retention_enabled: true delete_request_store: s3 -
Restart the compactor after changing the configuration.
Properties:
- Enforced by: Compactor delete request handler
- Retryable: No (configuration must change)
- HTTP status: 400 Bad Request
- Configurable per tenant: No
Error: Invalid delete request time format
Error message:
invalid start time: require unix seconds or RFC3339 format
Or:
invalid end time: require unix seconds or RFC3339 format
Cause:
The start or end time in a delete request is not in a valid format.
Resolution:
-
Use Unix seconds or RFC3339 format:
# Unix seconds curl -X POST http://compactor:3100/loki/api/v1/delete \ -H "X-Scope-OrgID: my-tenant" \ -d "query={app=\"foo\"}" \ -d "start=1704067200" \ -d "end=1704153600" # RFC3339 curl -X POST http://compactor:3100/loki/api/v1/delete \ -H "X-Scope-OrgID: my-tenant" \ -d "query={app=\"foo\"}" \ -d "start=2024-01-01T00:00:00Z" \ -d "end=2024-01-02T00:00:00Z"
Properties:
- Enforced by: Compactor delete request handler
- Retryable: No (request must be fixed)
- HTTP status: 400 Bad Request
- Configurable per tenant: No
Error: Delete request already processed
Error message:
deletion of request which is in process or already processed is not allowed
Cause:
An attempt was made to cancel a delete request that is already being processed or has completed processing.
Resolution:
-
Check the status of the delete request:
curl -s http://compactor:3100/loki/api/v1/delete \ -H "X-Scope-OrgID: my-tenant" | jq -
Submit a new delete request if you need to delete additional data.
Properties:
- Enforced by: Compactor delete request handler
- Retryable: No
- HTTP status: 400 Bad Request
- Configurable per tenant: No
Error: Invalid max_interval for delete request
Error message:
invalid max_interval: valid time units are 's', 'm', 'h'
Or:
max_interval can't be greater than <configured-limit>
Or:
max_interval can't be greater than the interval to be deleted (<duration>)
Cause:
The max_interval parameter on a delete request has an invalid value, exceeds the configured delete_max_interval limit, or exceeds the time range of the delete request itself.
Resolution:
-
Use a valid time format with supported units (
s,m,h):curl -X POST http://compactor:3100/loki/api/v1/delete \ -H "X-Scope-OrgID: my-tenant" \ -d "query={app=\"foo\"}" \ -d "start=1704067200" \ -d "end=1704153600" \ -d "max_interval=1h"
Properties:
- Enforced by: Compactor delete request handler
- Retryable: No (request must be fixed)
- HTTP status: 400 Bad Request
- Configurable per tenant: No
Ruler errors
Ruler errors occur when evaluating alerting rules or recording rules.
Error: Invalid ruler evaluation config
Error message:
invalid ruler evaluation config: <details>
Cause:
The ruler evaluation mode configuration is invalid.
Resolution:
-
Use a valid evaluation mode:
ruler: evaluation: mode: local # Or "remote"
Properties:
- Enforced by: Ruler module initialization (
initRuleEvaluator) - Retryable: No
- HTTP status: N/A (startup failure)
- Configurable per tenant: No
Error: Ruler remote write config conflict
Error message:
ruler remote write config: both 'client' and 'clients' options are defined; 'client' is deprecated, please only use 'clients'
Cause:
Both the deprecated client and the new clients configuration options are set for ruler remote write.
Resolution:
-
Remove the deprecated config and use
clients:ruler: remote_write: # Remove this: # client: {} # Use this instead: clients: primary: url: http://prometheus:9090/api/v1/write
Properties:
- Enforced by: Ruler initialization (
NewRuler) - Retryable: No
- HTTP status: N/A (startup failure)
- Configurable per tenant: No
Error: Remote write enabled but no URL configured
Error message:
remote-write enabled but no clients URL are configured
Or when multiple clients are configured in the clients map and one entry is missing a URL:
remote-write enabled but client '<name>' URL for tenant <client-id> is not configured
Cause:
Remote write is enabled for the ruler but no destination URL is configured. The first variant occurs when the clients map is empty. The second occurs when a named entry in the clients map has no url set; <client-id> is the map key for that entry, not a tenant ID.
Resolution:
-
Configure the remote write URL:
ruler: remote_write: enabled: true clients: primary: url: http://prometheus:9090/api/v1/write -
Or disable remote write:
ruler: remote_write: enabled: false
Properties:
- Enforced by: Configuration validation
- Retryable: No
- HTTP status: N/A (startup failure)
- Configurable per tenant: No
Error: Rule result is not a vector or scalar
Error message:
rule result is not a vector or scalar
Cause:
A rule evaluation returned an unexpected result type. Both recording rules and alerting rules must produce vector or scalar results. A plain log-stream expression (one that returns log lines rather than a numeric metric) triggers this error in either rule type.
Resolution:
-
Check the rule expression returns a vector or scalar:
# Valid - returns vector: record: my_metric expr: sum(rate({job="app"}[5m] | json | level="error")) # Invalid - returns logs (triggers error for both recording and alerting rules): # record: my_metric # expr: '{job="app"}' -
Use aggregation functions to produce numeric results from log queries.
Properties:
- Enforced by: Ruler evaluation
- Retryable: No (rule must be fixed)
- HTTP status: N/A (background process)
- Configurable per tenant: No
Error: Ruler WAL closed
Error message:
WAL storage closed
Cause:
An operation was attempted on the ruler's write-ahead log (WAL) after it was closed. This typically occurs during shutdown.
Resolution:
- Wait for the ruler to restart if it's restarting.
- Check ruler logs for errors that caused unexpected WAL closure.
- Verify disk space is available for WAL operations.
Properties:
- Enforced by: Ruler WAL
- Retryable: Yes (after ruler restart)
- HTTP status: N/A
- Configurable per tenant: No
Kafka integration errors
These errors occur when Loki is configured to use Kafka for ingestion.
Error: Missing Kafka address
Error message:
the Kafka address has not been configured
Cause:
Kafka ingestion is enabled but no Kafka broker address is configured.
Resolution:
-
Configure the Kafka address:
kafka_config: topic: loki-logs reader_config: address: kafka:9092 writer_config: address: kafka:9092
Properties:
- Enforced by: Configuration validation
- Retryable: No
- HTTP status: N/A (startup failure)
- Configurable per tenant: No
Error: Missing Kafka topic
Error message:
the Kafka topic has not been configured
Cause:
Kafka ingestion is enabled but no topic name is configured.
Resolution:
-
Configure the Kafka topic:
kafka_config: topic: loki-logs reader_config: address: kafka:9092 writer_config: address: kafka:9092
Properties:
- Enforced by: Configuration validation
- Retryable: No
- HTTP status: N/A (startup failure)
- Configurable per tenant: No
Error: Inconsistent SASL username and password
Error message:
both sasl username and password must be set
Cause:
Only one of the Simple Authentication and Security Layer (SASL) username or password is configured. Both must be set together.
Resolution:
-
Configure both username and password:
kafka_config: sasl_username: my-user sasl_password: ${KAFKA_PASSWORD} -
Or remove both if SASL authentication is not required.
Properties:
- Enforced by: Configuration validation
- Retryable: No
- HTTP status: N/A (startup failure)
- Configurable per tenant: No
Error: Kafka enabled in distributor but not in ingester
Error message:
kafka is enabled in distributor but not in ingester
Cause:
Kafka is configured for the distributor but the ingester isn't configured to read from Kafka. Both must be configured together.
Resolution:
-
Enable Kafka in both distributor and ingester:
distributor: kafka_writes_enabled: true ingester: kafka_ingestion: enabled: true
Properties:
- Enforced by: Configuration validation
- Retryable: No
- HTTP status: N/A (startup failure)
- Configurable per tenant: No
Error: Invalid producer max record size bytes
Error message:
the configured producer max record size bytes must be a value between 1048576 and 15983616
Cause:
The kafka_config.producer_max_record_size_bytes value is outside the allowed range.
Resolution:
-
Set
producer_max_record_size_byteswithin the allowed range:kafka_config: producer_max_record_size_bytes: 15983616 -
We strongly recommend leaving this value at its default unless you're testing in a development environment.
Properties:
- Enforced by: Configuration validation
- Retryable: No
- HTTP status: N/A (startup failure)
- Configurable per tenant: No
Error: Invalid producer max inflight requests per broker
Error message:
the configured producer max inflight requests per broker must be greater than or equal to 1
Cause:
The kafka_config.producer_max_inflight_requests_per_broker value is less than 1.
Resolution:
-
Set
producer_max_inflight_requests_per_brokerto1or higher:kafka_config: producer_max_inflight_requests_per_broker: 20
Properties:
- Enforced by: Configuration validation
- Retryable: No
- HTTP status: N/A (startup failure)
- Configurable per tenant: No
Error: Invalid producer linger
Error message:
the configured producer linger must be greater than or equal to 0
Cause:
The kafka_config.producer_linger value is negative.
Resolution:
-
Set
producer_lingerto0or a positive duration:kafka_config: producer_linger: 50ms
Properties:
- Enforced by: Configuration validation
- Retryable: No
- HTTP status: N/A (startup failure)
- Configurable per tenant: No
Error: Partition ring required for Kafka writes
Error message:
partition ring is required for kafka writes
Cause:
distributor.kafka_writes_enabled is true, but the distributor wasn't started with a partition ring. This usually means the partition ring key-value store isn't configured consistently across distributors and ingesters.
Resolution:
-
Configure the partition ring key-value store on both distributors and ingesters:
ingester: kafka_ingestion: enabled: true partition_ring: kvstore: store: memberlist -
Ensure every component that participates in Kafka-based ingestion (distributors, ingesters, queriers, and rulers) uses the same key-value store configuration.
Properties:
- Enforced by: Distributor startup
- Retryable: No
- HTTP status: N/A (startup failure)
- Configurable per tenant: No
Error: Single entry size exceeds maximum
Error message:
single entry size (%d) exceeds maximum allowed size (%d)
Cause:
A single log line is larger than the configured kafka_config.producer_max_record_size_bytes. Loki splits large write requests into multiple Kafka records, but it can't split a single log line across records.
Resolution:
-
Reduce the size of the log line at the source before sending it to Loki.
-
Enforce a maximum log line size with
limits_config.max_line_size(default: 256KB). Lines that exceed this limit are discarded before they reach the Kafka producer. Setlimits_config.max_line_size_truncate: trueto truncate oversized lines instead of discarding them:limits_config: max_line_size: 256KB max_line_size_truncate: true -
Increase
producer_max_record_size_bytes, within the allowed range, if your Kafka cluster supports larger records:kafka_config: producer_max_record_size_bytes: 15983616
Properties:
- Enforced by: Kafka record encoding
- Retryable: No
- HTTP status: 400 (Bad Request)
- Configurable per tenant: No (
limits_config.max_line_sizeandlimits_config.max_line_size_truncateare configurable per tenant)
Error: Dataobj builder topic required
Error message:
topic is required
Cause:
The experimental data object builder, used for columnar storage, is enabled but no Kafka topic is configured for it.
Resolution:
-
Configure the topic for the data object builder:
dataobj: builder: topic: loki-logs
Properties:
- Enforced by: Configuration validation
- Retryable: No
- HTTP status: N/A (startup failure)
- Configurable per tenant: No
Error: Ingest-limits configuration errors
Error messages:
num partitions must be greater than 0
consumer group must be set
topic must be set
Cause:
The optional ingest-limits service is enabled (ingest_limits.enabled: true), but one or more of its required Kafka settings are missing or invalid.
Resolution:
-
Configure all required ingest-limits Kafka settings:
ingest_limits: enabled: true num_partitions: 64 consumer_group: ingest-limits topic: loki-ingest-limits
Properties:
- Enforced by: Configuration validation
- Retryable: No
- HTTP status: N/A (startup failure)
- Configurable per tenant: No
Error: Ingest-limits readiness errors
Error messages:
waiting to be assigned some partitions
partitions are not ready
Cause:
The ingest-limits service hasn't finished joining the partition ring yet, or one or more of its assigned partitions aren't ready. This is usually a transient condition during startup or a rolling restart.
Resolution:
- Wait for the service to finish starting. These errors should clear on their own once the partition ring stabilizes.
- If the errors persist, check connectivity between the ingest-limits service and the Kafka brokers, and check that the configured partition ring key-value store is reachable.
Properties:
- Enforced by: Ingest-limits service readiness check
- Retryable: Yes (transient during startup)
- HTTP status: 503 (Service Unavailable)
- Configurable per tenant: No
Error: Generic Kafka protocol errors
Error messages:
unexpected response type
malformed response
Cause:
The Kafka client received a response from the broker that it didn't expect, or couldn't parse. This usually points to a Kafka, or Kafka-protocol-compatible broker, that uses a protocol version Loki doesn't fully support, or to a network issue that corrupted the response.
Resolution:
- Check your Kafka broker version against the version supported by Loki's Kafka client (franz-go).
- Check network connectivity between Loki and the Kafka brokers for packet loss or truncation.
Properties:
- Enforced by: Kafka client (offset manager)
- Retryable: Yes
- HTTP status: N/A
- Configurable per tenant: No
Bloom gateway errors
Bloom gateway errors occur when using bloom filters for query acceleration.
Error: Invalid bloom gateway addresses
Error message:
addresses requires a list of comma separated strings in DNS service discovery format with at least one item
Cause:
The bloom_gateway.client.addresses configuration field is empty or unset.
Resolution:
-
Configure valid addresses:
bloom_gateway: client: addresses: dns+bloom-gateway:9095Valid formats:
dns+hostname:port- DNS-based discoveryhost1:port,host2:port- Static list
Properties:
- Enforced by: Configuration validation
- Retryable: No
- HTTP status: N/A (startup failure)
- Configurable per tenant: No
Error: Request time range must span exactly one day
Error message:
request time range must span exactly one day
Cause:
Bloom gateway requests must be for exactly one day of data due to how bloom blocks are organized.
Resolution:
- This is typically handled automatically by the bloom querier, which splits multi-day queries into per-day requests before sending them to the gateway. If you see this error:
- Check that the querier is properly configured
- Ensure queries are routed through the querier
Properties:
- Enforced by: Bloom gateway
- Retryable: No
- HTTP status: 500 Internal Server Error
- Configurable per tenant: No
Error: From time must not be after through time
Error message:
from time must not be after through time
Cause:
The bloom gateway received a request where the start time (from) is later than the end time (through).
Resolution:
- This indicates a malformed request reaching the bloom gateway. Verify that the client sending the request constructs time ranges correctly with
from≤through.
Properties:
- Enforced by: Bloom gateway
- Retryable: No
- HTTP status: 500 Internal Server Error
- Configurable per tenant: No
Write-ahead log (WAL) errors
WAL errors occur when the ingester cannot properly manage its write-ahead log.
Error: WAL is stopped
Error message:
wal is stopped
Cause:
An operation was attempted on the WAL after it was stopped. This typically occurs during shutdown or after a fatal error.
Resolution:
- Check ingester health and logs for errors.
- Verify disk space is available.
- Restart the ingester if it's in a bad state.
Properties:
- Enforced by: Ingester WAL
- Retryable: Yes (after ingester restart)
- HTTP status: 500 Internal Server Error
- Configurable per tenant: No
Error: Invalid checkpoint duration
Error message:
invalid checkpoint duration: <duration>
Cause:
The WAL checkpoint duration is set to an invalid value (likely zero or negative).
Resolution:
-
Set a valid checkpoint duration:
ingester: wal: checkpoint_duration: 5m
Properties:
- Enforced by: Configuration validation
- Retryable: No
- HTTP status: N/A (startup failure)
- Configurable per tenant: No
Error: Invalid disk full threshold
{{< admonition type="note" >}}
The disk_full_threshold configuration option was introduced in Loki 3.7. This error does not occur in earlier releases.
{{< /admonition >}}
Error message:
invalid disk full threshold: <value> (must be between 0 and 1)
Cause:
The WAL disk full threshold is set to a value outside the valid range. Valid values are between 0 and 1 (inclusive), where 0 disables throttling and values greater than 0 represent the fraction of disk capacity at which writes are throttled.
Resolution:
-
Set a valid threshold:
ingester: wal: disk_full_threshold: 0.9 # 90%
Properties:
- Enforced by: Configuration validation
- Retryable: No
- HTTP status: N/A (startup failure)
- Configurable per tenant: No
Ingester lifecycle errors
Ingester lifecycle errors occur during ingester startup, shutdown, or state transitions.
Error: Ingester is shutting down
Error message:
Ingester is shutting down
Cause:
The ingester is in the process of shutting down and is no longer accepting writes. This error (also known as ErrReadOnly) is returned when a push request arrives during graceful shutdown. During this period the ingester may still serve reads for data it holds in memory.
Resolution:
- Configure clients to retry with backoff. The distributor will route to other healthy ingesters.
- Wait for shutdown to complete and the new instance to start.
- Check if shutdown is expected (rolling update, scaling event).
- If unexpected, check orchestrator logs for OOM kills or health check failures.
Properties:
- Enforced by: Ingester
- Retryable: Partial. The distributor sends writes to all ingesters in the replication set in parallel and uses a quorum model. If the remaining ingesters meet the minimum success threshold, the overall write succeeds despite this error from a shutting-down ingester.
- HTTP status: 500 Internal Server Error
- Configurable per tenant: No
Error: Ingester is stopping or already stopped
Error message:
Ingester is stopping or already stopped.
Cause:
The ingester's shutdown management endpoint (POST /loki/api/v1/ingester/shutdown) was called when the ingester was not in a Running state. This happens when the endpoint is called a second time during an in-progress shutdown or after the ingester has already stopped. This error is returned by the shutdown endpoint, not by the log-write or query paths.
Resolution:
- Do not call the shutdown endpoint again while a shutdown is already in progress.
- Check orchestrator for duplicate shutdown signals or restart policies.
- Investigate if the stop was unexpected (pod eviction, OOM, crash).
Properties:
- Enforced by: Ingester shutdown endpoint
- Retryable: No (the shutdown endpoint call itself is not retryable; wait for the ingester to restart before sending new writes)
- HTTP status: 503 Service Unavailable (response from the shutdown endpoint)
- Configurable per tenant: No
Error: Failed to start partition reader
Error message:
failed to start partition reader: <details>
Cause:
The ingester could not start its Kafka partition reader. This occurs when Kafka ingestion is enabled but the partition reader fails to initialize.
Resolution:
-
Check Kafka connectivity from the ingester.
-
Verify Kafka topic exists and the ingester has appropriate permissions.
-
Review Kafka configuration:
kafka: address: kafka:9092 topic: loki-logs -
Check Kafka broker health.
Properties:
- Enforced by: Ingester startup
- Retryable: No (configuration or infrastructure must be fixed)
- HTTP status: N/A (startup failure)
- Configurable per tenant: No
Error: Failed to start partition ring lifecycler
Error message:
failed to start partition ring lifecycler: <details>
Cause:
The ingester could not start its Kafka partition ring lifecycler during startup. This is a separate component from the partition reader; it manages the ingester's membership in the partition ring. This only occurs when Kafka ingestion is enabled.
Resolution:
- Check Kafka connectivity from the ingester.
- Verify the partition ring KV store (the store used for the partition ring) is reachable.
- Review ingester logs for the wrapped error in
<details>. - Check Kafka broker health and partition availability.
Properties:
- Enforced by: Ingester startup
- Retryable: No (configuration or infrastructure must be fixed)
- HTTP status: N/A (startup failure)
- Configurable per tenant: No
Error: Lifecycler failed
Error message:
lifecycler failed: <details>
Cause:
The ingester's lifecycler (which manages ring membership) encountered a fatal error. This prevents the ingester from participating in the ring.
Resolution:
- Check KV store connectivity (Consul, etcd, or memberlist).
- Review ingester logs for the specific lifecycler error.
- Verify ring configuration is consistent across all ingesters.
- Restart the ingester after fixing the underlying issue.
Properties:
- Enforced by: Ingester lifecycler
- Retryable: Yes (after fix and restart)
- HTTP status: N/A (internal failure)
- Configurable per tenant: No
Pattern ingester errors
Pattern ingester errors occur when using the pattern ingester for automatic log pattern detection.
Error: Pattern ingester replication factor must be 1
Error message:
pattern ingester replication factor must be 1
Cause:
The pattern ingester is configured with a replication factor other than 1. Currently, the pattern ingester only supports a replication factor of 1.
Resolution:
-
Set the replication factor to 1:
pattern_ingester: lifecycler: ring: replication_factor: 1
Properties:
- Enforced by: Configuration validation
- Retryable: No (configuration must be fixed)
- HTTP status: N/A (startup failure)
- Configurable per tenant: No
Error: Pattern ingester retain-for too short
Error message:
retain-for (<duration>) must be greater than or equal to chunk-duration (<duration>)
Cause:
The pattern ingester's retain_for duration is shorter than max_chunk_age, which would cause data loss.
Resolution:
-
Increase the retain-for duration to be at least as long as
max_chunk_age:pattern_ingester: retain_for: 15m # Must be >= max_chunk_age max_chunk_age: 5m
Properties:
- Enforced by: Configuration validation
- Retryable: No (configuration must be fixed)
- HTTP status: N/A (startup failure)
- Configurable per tenant: No
Error: Pattern ingester chunk-duration too short
Error message:
chunk-duration (<duration>) must be greater than or equal to sample-interval (<duration>)
Cause:
The pattern ingester's max_chunk_age is shorter than pattern_sample_interval. Chunks must span at least one sample interval to hold any data.
Resolution:
-
Increase
max_chunk_ageto be at least as long aspattern_sample_interval:pattern_ingester: max_chunk_age: 1h # Must be >= pattern_sample_interval (default: 1h) pattern_sample_interval: 10s # default: 10s
Properties:
- Enforced by: Configuration validation
- Retryable: No (configuration must be fixed)
- HTTP status: N/A (startup failure)
- Configurable per tenant: No
Error: Pattern ingester volume threshold out of range
Error message:
volume_threshold (<value>) must be between 0 and 1
Cause:
The volume_threshold value is outside the valid range of 0 to 1. This setting controls what fraction of log volume the pattern ingester tracks — only patterns representing the top X% of log volume are persisted.
Resolution:
-
Set
volume_thresholdto a value between 0 and 1 (default is0.99):pattern_ingester: volume_threshold: 0.99
Properties:
- Enforced by: Configuration validation
- Retryable: No (configuration must be fixed)
- HTTP status: N/A (startup failure)
- Configurable per tenant: No
API parameter errors
These errors occur when API requests contain invalid parameters.
Error: Invalid direction
Error message:
invalid direction '<value>'
Cause:
The direction query parameter contains an invalid value.
Resolution:
-
Use a valid direction value:
forward- Oldest to newestbackward- Newest to oldest (default)
curl "http://loki:3100/loki/api/v1/query_range?query={job=\"app\"}&direction=forward"
Properties:
- Enforced by: API handler
- Retryable: No (request must be fixed)
- HTTP status: 400 Bad Request
- Configurable per tenant: No
Error: Limit must be a positive value
Error message:
limit must be a positive value
Cause:
The limit parameter is zero or negative.
Resolution:
-
Provide a positive limit:
curl "http://loki:3100/loki/api/v1/query_range?query={job=\"app\"}&limit=100"
Properties:
- Enforced by: API handler
- Retryable: No (request must be fixed)
- HTTP status: 400 Bad Request
- Configurable per tenant: No
Error: End timestamp must not be before start time
Error message:
end timestamp must not be before or equal to start time
Cause:
The query's end time is before or equal to its start time.
Resolution:
-
Ensure end time is after start time:
curl "http://loki:3100/loki/api/v1/query_range?\ query={job=\"app\"}&\ start=2024-01-01T00:00:00Z&\ end=2024-01-02T00:00:00Z"
Properties:
- Enforced by: API handler
- Retryable: No (request must be fixed)
- HTTP status: 400 Bad Request
- Configurable per tenant: No
Error: Delay for tailing too large
Error message:
delay_for can't be greater than <max>
Cause:
The delay_for parameter for tailing queries exceeds the maximum allowed value.
Resolution:
-
Reduce the delay_for value:
curl "http://loki:3100/loki/api/v1/tail?query={job=\"app\"}&delay_for=5"The maximum value is typically 5 seconds.
Properties:
- Enforced by: API handler
- Retryable: No (request must be fixed)
- HTTP status: 400 Bad Request
- Configurable per tenant: No
Error: Query filtering requires compactor address
Error message:
query filtering for deletes requires 'compactor_grpc_address' or 'compactor_address' to be configured
Cause:
Query-time filtering for delete requests is enabled but Loki doesn't know how to reach the compactor to retrieve active delete requests.
Resolution:
-
Configure the compactor address:
compactor: compactor_grpc_address: compactor:9095 -
Or use the HTTP address:
compactor: compactor_address: http://compactor:3100
Properties:
- Enforced by: Module initialization
- Retryable: No (configuration must be fixed)
- HTTP status: N/A (startup failure)
- Configurable per tenant: No