API Gateway, Rate Limiting & Quota Enforcement for OGC Services
A geospatial portal has no natural upper bound on what a single caller can ask it to do. One client can request a hundred tiles per second across a viewport, ask for every feature in a national dataset in one GetFeature, or loop over a coverage at the deepest zoom for hours — all of it legitimate protocol usage, all of it capable of consuming the whole renderer. Rate limiting and quota enforcement are how a portal converts that open-ended exposure into something it can plan capacity for, and they belong at the gateway because that is the only place where the caller is already identified and the request has not yet cost anything. This topic sits inside the Core Portal Architecture & Security Boundaries framework, and it is the back-pressure mechanism that the resilience patterns elsewhere in that framework assume exists.
The mistake that makes these controls unpopular is treating them purely as abuse prevention. In an agency portal, the caller consuming ninety percent of the renderer is almost never an attacker: it is a partner organisation’s nightly harvest, an enthusiastic contractor’s mapping application, or a colleague’s script with no delay between requests. Limits exist so that those callers can be told what they may have, so that the portal keeps serving everybody else while one of them is misbehaving, and so that capacity planning has a number to plan against.
Not All Requests Cost the Same
The unit almost every rate limiter counts is the request, and for OGC traffic that unit is close to meaningless. A cached tile costs a lookup; an uncached tile over a dense extent costs hundreds of milliseconds of rendering; a GetFeature with no count limit over a national parcel dataset costs a table scan, several gigabytes of serialisation, and enough transfer to saturate an uplink. A limit of a thousand requests per minute permits all three, so it protects nothing in the case that matters.
The practical resolution is to count cost rather than calls: assign each operation class a weight, deduct the weight from the caller’s budget, and let one expensive operation consume as much of the budget as many cheap ones. A tile from cache might cost one unit, an uncached render ten, a bounded feature query twenty, and an unbounded one be rejected outright in favour of a paged interface. The weights do not need to be precise — they need only be in the right order, so that a caller cannot convert a generous tile allowance into an unlimited feature-extraction allowance.
Two operation classes deserve special handling. Capabilities documents are requested constantly by clients that refresh them on every session; they should be cheap because they are cached, and if they are not cached the limit needs to reflect the real cost, as described in caching GetCapabilities responses at the edge. Write operations — a WFS transaction — are rare, expensive, and consequential, and belong on a separate, much smaller budget from reads so that a runaway editing client cannot exhaust the allowance that serves the public map.
Identifying the Caller Before Deciding the Limit
A limit is only as good as the identity it is applied to. Limiting by source address is the default because it needs no cooperation, and it is wrong in both directions for an agency portal: an entire government department behind one outbound address is treated as one caller and throttled collectively, while a distributed client trivially exceeds any per-address limit by spreading its requests. Both failures are common enough that address-based limiting should be treated as a backstop rather than as the policy.
The workable arrangement for a portal that serves both public and authenticated traffic is a layered one. Anonymous callers get a modest limit keyed on address, sized so that a shared organisational address is uncomfortable but not blocked, with a clear route to obtaining a key. Keyed callers get a limit attached to the key, so revoking or adjusting one client is a single operation. Authenticated callers get a limit attached to the tenant, because that is the unit the portal has an agreement with — and because a tenant’s own users competing for the tenant’s allowance is a problem the tenant can reason about, while a global limit that one tenant exhausts is a problem only the portal can see. The identity mechanics for the third layer are covered in Keycloak vs Dex for OIDC federation, and the per-key mechanics in issuing scoped API keys for OGC clients.
Rate, Burst and Quota Are Three Different Controls
Teams frequently implement one of these and describe it as all three. They answer different questions and a portal generally needs all of them.
A rate bounds sustained throughput — requests per second, averaged over a short window. It is what keeps a single client from occupying the renderer indefinitely. A burst allowance permits short excursions above the rate, and it is not a nicety: a slippy map legitimately opens thirty connections at once when a user pans, and a rate limit without burst tolerance makes normal interactive use fail while a steady scripted crawl passes. A quota bounds total consumption over a long period — a day, a month — and it is the only one of the three that can express “this partner may extract the dataset once a week, not continuously”.
# /etc/nginx/conf.d/ogc-limits.conf
# Cost-weighted limiting keyed on the resolved caller, with a burst that
# matches how a slippy map actually behaves.
# Resolve the caller: authenticated tenant first, then API key, then address.
map $http_authorization$http_x_api_key $ogc_caller {
default $binary_remote_addr; # anonymous fallback
"~^Bearer" $jwt_claim_tenant; # set by the auth subrequest
"~^.+$" $http_x_api_key;
}
# Operation class drives which zone the request is charged against.
map $arg_request $ogc_class {
default tiles;
"~*^GetMap$" tiles;
"~*^GetTile$" tiles;
"~*^GetFeature$" features;
"~*^Transaction$" writes;
}
limit_req_zone $ogc_caller zone=ogc_tiles:20m rate=40r/s;
limit_req_zone $ogc_caller zone=ogc_features:10m rate=2r/s;
limit_req_zone $ogc_caller zone=ogc_writes:10m rate=1r/s;
limit_req_status 429;
server {
listen 443 ssl;
server_name portal.example.gov;
location /ows/ {
# A viewport of tiles is a legitimate burst; a sustained crawl is not.
limit_req zone=ogc_tiles burst=60 nodelay;
limit_req zone=ogc_features burst=4;
limit_req zone=ogc_writes burst=2;
# Tell the caller what happened and when to come back.
add_header Retry-After 5 always;
proxy_pass http://ogc_backend;
}
}
The nodelay on the tile zone is the setting that makes interactive maps work. Without it, a burst is queued and released at the configured rate, so the thirty tiles of a pan arrive over most of a second and the map visibly fills in; with it, the burst is admitted immediately and only sustained excess is rejected. The feature zone deliberately omits it, because a burst of expensive queries has no interactive justification and queuing them is the desired behaviour.
What a Rejected Caller Should Be Told
A limit that rejects without explanation produces a support ticket and a client that retries immediately, which is the opposite of what the limit is for. Three response elements turn a rejection into a behaviour change: the correct status, a Retry-After telling the client when the budget will have recovered, and headers stating the limit, the remaining allowance and the reset time so a well-written client can pace itself without ever being rejected.
Note that OGC clients vary enormously in how they treat a 429. Browser-based mapping libraries generally retry, desktop GIS often surfaces a blank layer, and harvesting scripts frequently treat any non-200 as fatal and abandon the run. Because of that spread, the limits for keyed and authenticated callers should be set high enough that a correctly behaved client never meets them in normal use — the limit is there to bound a pathological case, not to shape everyday traffic.
Where the Limit Must Sit in the Request Path
Placement decides whether the control saves anything. A limit evaluated after authentication has already paid for a token validation; a limit evaluated after routing has already occupied a worker; a limit evaluated in the application has already done everything except the render. For the cheap-rejection property to hold, the check must happen at the earliest point at which the caller can be identified — which is why API keys and address-based limits are checked at the edge, and token-derived tenant limits immediately after token validation and before any proxying.
There is one deliberate exception. Limits that depend on authorisation — a per-tenant quota that differs by subscription, or a rule that only applies to restricted layers — cannot be evaluated before the policy decision. Those belong immediately after the authorisation check and before the request is proxied, and they should be counted separately so that a rejection there is distinguishable in the logs from an edge rejection. The boundary model this fits into is set out in security boundary mapping for OGC services.
Operating the Limits After They Are Set
Rate limits are not a configuration you set once. Traffic grows, clients change, and a limit that was generous last year is now the reason a partner’s integration fails intermittently. Three operational practices keep them honest.
Record every rejection with the caller identity, the class, and the limit that fired, and review the distribution weekly rather than only during incidents. A single caller producing all the rejections is a conversation; rejections spread across many callers mean the limit is wrong.
Run the limits in observation mode before enforcing them. Count what would have been rejected and for whom, leave it for a full traffic cycle including a month-end, and only then enforce. The callers who would have been affected are exactly the ones to notify first.
Give yourself an exemption mechanism that is auditable rather than ad hoc. There will be a legitimate bulk extraction — a statutory reporting deadline, a partner’s annual refresh — and the choice is between a documented temporary allowance with an expiry and somebody quietly commenting out the limit at four in the afternoon.
Quotas Are a Conversation, Not Only a Control
The part of this topic that fails most often is not technical. A quota that a partner organisation has never been told about is experienced as an unexplained outage; a quota nobody reviews becomes either irrelevant or an obstacle; and a quota with no route to an exception gets bypassed by whoever is under the most pressure. Treating each allowance as an agreement with a named party, rather than as a number in a configuration file, is what keeps the mechanism working after its first year.
Three artefacts make that practical. A published table of tiers, so an integrator can see what they will get before they build anything. A record, per consumer, of which tier applies and who agreed it — the same record that carries the contact details used when something breaks. And a documented route to a temporary increase, with an expiry, so the statutory reporting deadline that needs ten times the usual extraction is handled by a dated exception rather than by disabling the limit.
The review cadence matters as much as the numbers. Traffic grows, and a tier set two years ago against a portal half the size is now either generous enough to be pointless or tight enough to be the reason a partner’s nightly job fails on Mondays. Reviewing the rejection distribution quarterly — which consumers are hitting limits, how often, and whether the limit is doing anything useful in each case — takes an hour and prevents both failure modes.
One further habit is worth adopting from the outset: never change a limit during an incident without recording it. Raising an allowance to relieve pressure is a reasonable response, and an allowance raised at three in the morning and never revisited is how a portal ends up with limits that permit exactly the behaviour they were introduced to prevent. Make the temporary change carry an expiry from the moment it is applied.
Operational Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Interactive maps stutter while scripted clients are unaffected | Burst allowance too small, or nodelay missing on the tile zone |
Size burst to a full viewport of tiles and admit it immediately |
| One department is throttled collectively | Limiting on source address behind a shared outbound gateway | Move that organisation to keyed or authenticated identity |
| A client retries immediately and stays blocked | No Retry-After, or the client ignores it |
Emit Retry-After, and publish budget headers so the client can pace itself |
| Renderer still saturates despite limits being enforced | Limits count requests, not cost — feature queries pass the tile budget | Weight by operation class and give features their own, smaller zone |
| Limits have no effect on a distributed client | Per-address keying with traffic spread across many addresses | Require a key for programmatic access; keep the address limit as a backstop |
| A quota resets at an unexpected moment | Fixed-window quota with a boundary the client has learned to exploit | Use a rolling window, or randomise the window start per caller |
| Rejections spike after a release with no traffic change | A client library began retrying internally, multiplying every request | Correlate rejection counts with client version; cap retries at the client |
FAQ
Should a public portal rate-limit anonymous traffic at all?
Yes, but gently, and with a route out. An anonymous limit exists to keep one scripted client from consuming the capacity that the public map needs, not to discourage use. Set it high enough that a person browsing a map never meets it, publish how to obtain a key for anything programmatic, and treat repeated anonymous rejections from one network as a prompt to contact whoever is behind it rather than as an attack.
What is a reasonable starting point if there is no traffic history?
Take the renderer’s measured throughput, decide what share one caller may consume at peak — a tenth is a defensible starting point for a portal with more than ten consumers — and set the sustained rate from that. Set the burst to one full viewport of tiles, typically between thirty and sixty. Then run in observation mode for a full week before enforcing, because the real distribution is almost never what the estimate assumed.
Do rate limits replace the need for caching and capacity?
No. A limit bounds the worst case; it does nothing for the ordinary case, and a portal whose normal traffic sits near its limits is under-provisioned rather than well protected. The order of work is capacity first, caching second, limits third — limits are what keep the first two from being consumed by a single caller, not a substitute for either.
How should a quota interact with a bulk export request?
Deliberately, and outside the interactive path. Bulk extraction has a legitimate place in an agency portal, and forcing it through the same budget as interactive traffic guarantees either that the export fails or that the limit is too loose to protect anything. Provide a separate route — a paged interface, a scheduled export, or a snapshot download — with its own allowance, and point the rejection message at it.
What happens to the limits when the rate-limit service is down?
Decide this before it happens, and prefer failing open for read traffic on a public portal: an auxiliary control’s outage should not become the portal’s outage. Failing open means the limiter needs its own alerting, since a silent failure otherwise looks exactly like a quiet week. Write operations are the reasonable exception, where failing closed is usually the safer default.
Related
- Enforcing Per-Tenant WMS Request Quotas with Nginx — the tenant-keyed configuration in full.
- Throttling WFS GetFeature with Envoy Rate Limits — a service-mesh implementation for feature traffic.
- Issuing Scoped API Keys for OGC Clients — giving each integration its own revocable identity.
- Shielding GeoServer from Oversized BBOX Requests — rejecting a request by its shape before it becomes a render.
- Security Boundary Mapping for OGC Services — where this control sits among the portal’s other boundaries.
Up one level: Core Portal Architecture & Security Boundaries.