In HAProxy, what do the `size`, `expire` and `store` parameters of a `stick-table` line control, and what happens to new clients once the table is full?
answer
- size counts entries, not bytes
- expire is an idle timer
- period lives inside the stored counter
- oldest entries purged when full
- undersized table fails silently
basics
~20 ssize is the maximum number of entries, expire the idle timeout per entry, and store the list of counters each entry carries, with the measurement window declared inside the counter. When the table is full HAProxy purges the oldest entries to make room, unless nopurge is set.
solid answer
~50 sA line such as `stick-table type ip size 100k expire 10m store http_req_rate(10s),conn_cur` declares a table of at most one hundred thousand entries, each keyed by an IPv4 address and dropped after ten minutes without a match. `store` says what the entry carries beyond the server id: here a sliding request rate measured over ten seconds and the current connection count. That period lives in the `store` declaration, not in the rule that later compares the value. Memory follows from `size` — roughly fifty bytes per entry plus the stored data types — and is the reason you cannot simply set it huge. When the table reaches `size`, HAProxy by default purges the oldest entries to make room for new keys; declaring the table `nopurge` reverses that, so existing entries survive and new keys simply cannot be tracked. Either way an undersized table degrades silently: no error, just clients losing their entry early.
code
ini · 4 linesfrontend fe_public
bind :80
stick-table type ip size 100k expire 10m store http_req_rate(10s),conn_cur
default_backend appgo deeper
Recall that size is a number of entries, expire is how long an idle entry lasts, and store lists the counters an entry keeps beyond the chosen server.
Explain that the measurement window is declared inside the stored counter rather than at the rule that reads it, and describe the default purge-the-oldest behaviour when the table fills.
Diagnose the silent failure: a table too small for the live key count evicts entries before their window closes, so limits never fire and pins are lost. Show how you would size it from distinct keys per expire window and verify against the live entry count.
Own the memory budget across the fleet — how many tables, how many keys, what expire buys you versus a larger size — and set the standard for which keys are worth tracking at the edge at all rather than deeper in the stack.
## The declaration ``` frontend fe_public bind :80 stick-table type ip size 100k expire 10m store http_req_rate(10s),conn_cur ``` Each parameter answers a different question. **`type`** — what the key is. `ip` and `ipv6` for addresses, `integer`, `string` (usually with `len` to cap how much is kept) and `binary` for everything else. The type has to match the sample expression you later track on. **`size`** — the maximum number of entries, with `k`, `m` and `g` suffixes counting entries, not bytes. This is the parameter most often misread. The memory implication is roughly fifty bytes of overhead per entry plus the size of each stored data type, so a hundred thousand entries with a couple of counters is a few megabytes, and ten million is not. **`expire`** — how long an entry lives without being touched. It is refreshed on creation, on a match and on an update, so it is an inactivity timeout rather than a lifetime. **`store`** — the extra data types each entry carries. Common ones are `conn_cnt`, `conn_cur`, `conn_rate(<period>)`, `sess_rate(<period>)`, `http_req_cnt`, `http_req_rate(<period>)`, `http_err_rate(<period>)`, `bytes_in_rate(<period>)` and the general-purpose counters `gpc0` and `gpt0`. Without `store` the entry holds only the server id, which is all stickiness needs. ## The period belongs to the counter This trips people up: in `store http_req_rate(10s)` the ten seconds is part of the *stored counter's definition*. A later rule reading `sc_http_req_rate(0)` simply returns the current value of that ten-second rate; it has no way to ask for a different window. Wanting both a burst limit and a sustained limit means storing two counters with two periods, or two tables. Those rate counters are not exact bucket counts either — HAProxy maintains a moving average across the current and previous interval, so a value is a smoothed estimate. A short burst can slip under a threshold that a steady stream of the same volume would trip. ## Sizing, and what an undersized table does The rule of thumb is that `size` must comfortably exceed the number of distinct keys you expect to be live within `expire`. If you rate-limit per source address on a service that sees a million distinct addresses in ten minutes, a hundred-thousand-entry table means entries are being evicted continuously. The failure is quiet and nasty: an abusive client's entry can be purged before its ten-second window elapses, so its rate counter restarts from zero and the limit never fires. Nothing logs an error — the protection is simply not working. The same shortfall in a persistence table shows as users losing their pin at random, which surfaces as unexplained logouts if the application keeps session state locally. Oversizing is not free either: entries are allocated as needed rather than up front, but a table sized so generously that a scanner can fill it becomes its own memory pressure, which is exactly what `expire` is there to bound. Shortening `expire` is often a better lever than growing `size`. ## Full-table behaviour By default HAProxy makes room by purging the oldest entries — the ones nearest expiry — when it needs to create a new one. Adding `nopurge` to the declaration flips this: existing entries are kept and new keys are not stored at all. `nopurge` matters when the entries are more valuable than the newcomers, for example when the table records confirmed abusers that you do not want a flood of fresh addresses to wash away. ## Scope and lifetime The table belongs to the proxy section that declares it, and rules in another section can reach it with an explicit `table <proxy>` argument. It is per-process memory: two HAProxy nodes have two independent tables until a `peers` section is added, and its contents are gone if the process is replaced without a mechanism to hand them over. Treat the numbers as operational data, not as a system of record. Whatever the sizing, the runtime API's `show table` is how you check the live entry count and see whether you are near the ceiling.
- How would you enforce both a short-burst and a sustained-rate threshold?Each stored rate carries exactly one window, so you need a distinct stored value per window — typically a second table keyed the same way with a longer period, tracked into another sticky counter slot. Then write one condition per counter. Trying to reinterpret a single stored rate over a different interval is not something the fetch supports.
- When would you add `nopurge` to a stick table?When existing entries are worth more than new ones — a table that records clients already flagged as abusive, for instance. Without `nopurge`, a flood of fresh source addresses evicts the very entries you wanted to keep. The cost is that once the table is full, genuinely new keys are not tracked at all.
- Why might a per-IP rate limit never trigger even though the rule looks correct?Common causes are an undersized table whose entries are evicted before the window elapses, an `expire` shorter than the counter's period, or a key that collapses many clients into one entry. Checking the live entry count against `size` on the runtime API usually settles it in seconds.
saying these in an interview costs you the question
- Reading size as a memory figure in megabytes
- Thinking the rate window is set at the comparison rule
- Assuming a full table returns an error to the client
- Setting expire shorter than the counter's period
- Believing rate counters are exact request counts