Reputation: 3239
I have a server-side NodeJS application that uses a Google Service Account with Domain Delegation to create Google Calendar events on behalf of other users via the Google Calendar API batch endpoint.
Because I am attempting to create more than 600 events/minute (my current "Queries per minute per user" limit), I would like to specify other users for the quota to be charged to as described in the Proper accounting with service accounts section of the "Manage Quotas" article in in the Google Calendar API document, which states:
If your application is performing requests using domain-wide delegation, by default the service account is charged with regard to "per minute per project per user" quotas, and not the user you're impersonating. This means that the service account will likely run out of quota and be rate-limited, even though it might be operating on multiple users' calendars. You can avoid this by using the quotaUser URL parameter (or x-goog-quota-user HTTP header) to indicate which user will be charged. This is used only for quota calculations.
That documentation also links to Limiting requests per user in the Google Cloud APIs "Capping API usage" article, which states:
To prevent individual users from using up your API quota, some APIs include a default per user per minute limit. If such a default limit exists, you can modify that value as described in the previous section to limit the quota available to each user.
Individual users are identified by a unique string. If you're creating a server-side application (where the calling code is hosted on a server that you own) that makes requests on behalf of users, your requests must include the quotaUser parameter.
To identify a user, use the quotaUser=userID parameter. This value is for short-term quota enforcement only, so you don't need to use a real user ID. You can choose any arbitrary string under 40 characters long that uniquely identifies a user.
The quotaUser parameter is only used for capping requests per user per minute. If you don't send the quotaUser parameter, then all calls are attributed to your server machines, in which case calls can't be capped by user.
No matter how I try to include quotaUser
in my requests, however, I start receiving 403 errors like the following as soon as I make more than 600 requests in a minute, suggesting that the quotaUser
value is being ignored:
{
"body": {
"error": {
"code": 403,
"message": "Quota exceeded for quota metric 'Queries' and limit 'Queries per minute per user' of service 'calendar-json.googleapis.com' for consumer 'project_number:xxxxxxxxxxx'.",
"errors": [
{
"message": "Quota exceeded for quota metric 'Queries' and limit 'Queries per minute per user' of service 'calendar-json.googleapis.com' for consumer 'project_number:xxxxxxxxxxx'.",
"domain": "usageLimits",
"reason": "rateLimitExceeded"
}
]
"status": "PERMISSION_DENIED",
"details": [
{
"@type": "type.googleapis.com/google.rpc.ErrorInfo",
"reason": "RATE_LIMIT_EXCEEDED",
"domain": "googleapis.com",
"metadata": {
"service": "calendar-json.googleapis.com",
"quota_limit": "defaultPerMinutePerUser",
"quota_metric": "calendar-json.googleapis.com/default",
"consumer": "projects/xxxxxxxxxxx"
}
}
]
}
}
Not able to find a concrete example of how the quotaUser parameter should be included, I've tried every combination conceivable to no avail, including (and being sure to vary the actual value passed to quotaUser for each request):
In the query string of the request part url:
POST https://www.googleapis.com/batch/calendar/v3 HTTP/1.1
Authorization: /*Auth token*/
Content-Type: multipart/mixed; boundary=batch_foobarbaz
--batch_foobarbaz
Content-Type: application/http
Content-ID: <xxx>
POST /calendar/v3/calendars/primary/events?quotaUser=usernamemydomaincom
Content-Type: application/json
{{ body }}
--batch_foobarbaz--
In the request part:
POST https://www.googleapis.com/batch/calendar/v3 HTTP/1.1
Authorization: /*Auth token*/
Content-Type: multipart/mixed; boundary=batch_foobarbaz
--batch_foobarbaz
Content-Type: application/http
Content-ID: <xxx>
POST /calendar/v3/calendars/primary/events
Content-Type: application/json
quotaUser: usernamemydomaincom
{{ body }}
--batch_foobarbaz--
In the request part using the header name format:
POST https://www.googleapis.com/batch/calendar/v3 HTTP/1.1
Authorization: /*Auth token*/
Content-Type: multipart/mixed; boundary=batch_foobarbaz
--batch_foobarbaz
Content-Type: application/http
Content-ID: <xxx>
POST /calendar/v3/calendars/primary/events
Content-Type: application/json
X-Goog-Quota-User: usernamemydomaincom
{{ body }}
--batch_foobarbaz--
As well as in the request to the batch endpoint itself
POST https://www.googleapis.com/batch/calendar/v3?quotaUser=usernamemydomaincom HTTP/1.1
Authorization: /*Auth token*/
Content-Type: multipart/mixed; boundary=batch_foobarbaz
--batch_foobarbaz
Content-Type: application/http
Content-ID: <xxx>
POST /calendar/v3/calendars/primary/events
Content-Type: application/json
{{ body }}
--batch_foobarbaz--
and
POST https://www.googleapis.com/batch/calendar/v3 HTTP/1.1
Authorization: /*Auth token*/
Content-Type: multipart/mixed; boundary=batch_foobarbaz
X-Goog-Quota-User: usernamemydomaincom
--batch_foobarbaz
Content-Type: application/http
Content-ID: <xxx>
POST /calendar/v3/calendars/primary/events
Content-Type: application/json
{{ body }}
--batch_foobarbaz--
How can I properly provide the quotaUser
argument so I'm not subject to the 600 requests per user per minute limit?
Upvotes: 4
Views: 1274
Reputation: 2844
quotaUser is ignored when a principle user is passed. See How does quotaUser work? In order to pass 600 user quota limit you may have to impersonate as different user before you make an api call to the calendar.
const newAuths = userIds.map(
(userId) =>
new google.auth.JWT({
email: authProp.email, //service account
key: authProp.key,
scopes: SCOPES,
subject: userId, //<===This helped
})
);
// Now you can make 600 call with newAuths[0]
// and next 600 with newAuths[1] and so on,
// till you reach 10,000 upper limit set for you app.
Upvotes: 0
Reputation: 4534
I'm also looking for the same answer. Did you ever try using the principal as the value rather than the email address? I suspect the 40 character limit is a hint that that's what they expect. Maybe?
We're going to try and we'll update here if we find out!
Upvotes: 0
Reputation: 1333
If you are looking for an example you can check the Node JS library documentation for Global options:
const {google} = require('googleapis');
google.options({
// All requests from all services will contain the above query parameter
// unless overridden either in a service client or in individual API calls.
params: {
quotaUser: '[email protected]'
}
});
However as you are getting a RATE_LIMIT_EXCEEDED
another solution would be to implement Exponential Backoff as suggested in the documentation.
Upvotes: 2