Handle GitLab API Rate Limits

Build a GitLab API script that reads headers, backs off, and recovers from 429s.

Introduction

30 Second Summary

Automations can work perfectly until a service decides they have asked for too much too quickly. Without a recovery plan, one rejected request can stop the entire run.

In this project, you will build a script that exposes the quota behind every response from GitLab.com's REST API. The finished script uses a personal access token with the reset header to recover after 429.

What You'll Build

Run your script to watch the remaining quota fall until it reaches the limit. You will then see the script wait for the reset before recovering on its own.

By the end of this project, you'll have:

  • A live quota readout that shows the response status plus the rate limit headers after every call.
  • A clear anonymous and authenticated comparison showing how a personal access token changes the remaining count.
  • A recovery log showing the script reach 429. You will see it wait until reset before retrying once.
  • Secret Mission: Build a quota plot that shows the remaining count fall before rising again after the reset.

Are there any prerequisites?

You need a free GitLab.com account for the token step. You also need a programming language with an HTTP client available.

The project costs $0 because the account and public API endpoint are free to use.

Before We Start

Your rate-limit experiment needs a reliable way to send HTTP requests. A missing runtime would stop the project before you could inspect the first response.

This step prepares Python with the Requests library. You also confirm access to GitLab.com before the later token step.

In this step, get ready to:
  • Confirm the Python toolchain is available in your terminal.
  • Install Requests for the API calls.
  • Confirm access to your GitLab.com account.
Check Python and pip

Python runs the script you build later. The pip installer adds the Requests library to that Python environment.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Launch a terminal by typing Terminal (macOS) or PowerShell (Windows) and pressing Enter.

macOS

  • Check Python and pip by running these commands:
python3 --version
python3 -m pip --version

What do these commands check?

  • The first command confirms that your terminal can find Python.
  • The second command runs pip through the same Python installation.

Windows

  • Check Python and pip by running these commands:
python --version
python -m pip --version

What do these commands check?

  • The first command confirms that your terminal can find Python.
  • The second command runs pip through the same Python installation.

The first line should begin with Python 3. The next line should show a pip version plus its installation path.

✔️ I see Python and pip versions

Your terminal toolchain is ready. Python can run scripts while pip can add the library this project needs.

  • Continue to the Requests installation below.

ⓧ A command is not found

A missing command means Python needs to be installed before the project can continue. Choose your operating system below.

macOS

  • Visit the official Python releases for macOS page.
  • Select Download macOS installer under the latest stable release.
  • Launch the downloaded .pkg file from your Downloads folder.
  • Select Continue in the installer.
  • Select Agree after reviewing the license.
  • Select Install to use the standard installation.
  • Complete the administrator approval if macOS requests it.
  • Run Install Certificates.command from the new Python folder in Applications.
  • Return to the terminal from earlier and verify the installation by running:
python3 --version
python3 -m pip --version
What confirms the installation?

You should now see a Python release beginning with Python 3. The pip line confirms that package installation is available.

Still missing Python or pip?
  • Close the old terminal window after the installer finishes.
  • Launch a new terminal so it loads the updated command path.

Ask for help with my macOS Python installation.

Windows

  • Visit the official Python releases for Windows page.
  • Select Download Installer (MSIX) for the latest Python install manager.
  • Launch the downloaded installer from your Downloads folder.
  • Select Install.
  • Complete any Windows approval prompt that appears.
  • Return to the terminal from earlier after the installation finishes.
  • Verify the Python runtime and pip by running:
python --version
python -m pip --version
What confirms the installation?

You should now see a Python release beginning with Python 3. The pip line confirms that package installation is available.

Still missing Python or pip?
  • Close the old PowerShell window after the installer finishes.
  • Launch a new PowerShell window so it loads the updated command path.
  • Search for Manage app execution aliases from the Windows Start menu if the command remains unavailable.
  • Confirm that the Python (default) alias is enabled.

Ask for help with my Windows Python installation.

Install and verify Requests

Requests gives your script a direct way to call a web API. Installing it now prepares the HTTP client without creating the API script yet.

  • Install Requests through pip by running this command:
pip install requests

What does this command do?

Pip downloads Requests plus the supporting packages it requires. The library becomes available to Python scripts on your computer.

The installation can take a moment while pip downloads the package. Terminal output during this wait means the process is still working.

Did the Requests installation fail?

  • Confirm that your internet connection is active.
  • Confirm that the Python and pip checks from the previous substep still return version information.
  • Run the installation command again after correcting either issue.

Ask for help with my Requests installation.

Before you verify the installation, do you expect Python to print a Requests version or report a missing library?

macOS

  • Test the Requests import by running:
python3 -c "import requests; print(requests.__version__)"

What does this check prove?

Python imports Requests before printing its installed version. A version number proves the library is available to the interpreter you use for this project.

Windows

  • Test the Requests import by running:
python -c "import requests; print(requests.__version__)"

What does this check prove?

Python imports Requests before printing its installed version. A version number proves the library is available to the interpreter you use for this project.

You should see a Requests version number on its own line. That output confirms your HTTP client is ready for the first GitLab API call.

Confirm your GitLab.com account

A GitLab.com account is needed when you create a personal access token later. This check confirms that the account is available without creating a token early.

Your password stays on GitLab.com's own sign-in page, so this project never asks you to paste it into a script.

✔️ I already have an account

  • Complete the sign-in form with your existing GitLab credentials.
  • Select your avatar in the upper-right corner after signing in.
  • Select your name or username.

You should see your GitLab profile page. This proves the account is ready for the token step later in the project.

ⓧ I need an account

  • Select Register now on the sign-in page.
  • Complete the requested account details with a valid email address.
  • Verify your email using the message GitLab sends you.
  • Return to the GitLab.com sign-in page from earlier.
  • Complete the sign-in form with your new account.
  • Select your avatar in the upper-right corner.
  • Select your name or username.

You should see your new GitLab profile page. This confirms the account is available for the later token step.

Can't access your GitLab account?

  • Select Forgot your password? on the sign-in page if your password is not working.
  • Check your inbox for the verification message if registration has not completed.

Ask for help with my GitLab account access.

Before the final check, what two visible results prove that both your local toolchain and your GitLab access are ready?

  • Return to the terminal from earlier and confirm that your Requests version is still visible.
  • Return to your GitLab profile and confirm that your name or username is visible.

You now have a working Python environment with Requests installed. Your GitLab.com profile confirms that the account needed later is ready.

Your local tools and GitLab access are ready. Next up, you'll make an anonymous API call and inspect the rate-limit headers GitLab returns.

Call GitLab Without a Token

A rate limit only helps when your script can see it. This step turns headers from the GitLab.com REST API into terminal output.

The script uses Requests without an authentication token. Its HTTP status code plus quota headers create the anonymous baseline for every later comparison.

In this step, get ready to:
  • Create an unauthenticated script for the public GitLab endpoint.
  • Run the script to inspect the anonymous quota.
Create the anonymous API script

Response headers carry metadata about an API call. GitLab uses them to report the quota limit plus the number of requests left before reset.

  • Create a new file named gitlab_rate_limit.py on your Desktop with your code editor's new-file control.
  • Add the anonymous request plus the header reporting logic by pasting this code into gitlab_rate_limit.py:
import requests

# Target one public project response to keep the payload small
URL = "https://gitlab.com/api/v4/projects?per_page=1"

# List every GitLab rate-limit header the script can report
RATE_LIMIT_HEADERS = (
    "RateLimit-Limit",
    "RateLimit-Name",
    "RateLimit-Observed",
    "RateLimit-Remaining",
    "RateLimit-Reset",
    "RateLimit-ResetTime",
    "Retry-After",
)

# Send the request without authentication headers
response = requests.get(URL)

# Show the result before printing available quota details
print(f"HTTP status: {response.status_code}")
print("Rate limit headers:")
for header in RATE_LIMIT_HEADERS:
    value = response.headers.get(header)
    if value is not None:
        print(f"{header}: {value}")

What does this code do?

  • The URL value points to a public endpoint that returns one project per response.
  • The RATE_LIMIT_HEADERS tuple lists each documented GitLab rate-limit header that can appear.
  • The requests.get(URL) call sends the request without authentication headers.
  • The loop prints each header that GitLab included in the response.
  • Save gitlab_rate_limit.py on your Desktop.
  • Confirm that gitlab_rate_limit.py appears in your editor's file list.

Can't find the script on your Desktop?

Check that the file name ends with .py. Some text editors add a hidden .txt extension when saving.

Save the file directly to your Desktop if your editor placed it in a recent-files folder.

Help me find or correctly save my Python script.

✔️ Awesome, I've got everything!

Your anonymous request script is saved. Double-check that your latest changes are in gitlab_rate_limit.py.

ⓧ I'd like to double check the full code

Compare your complete gitlab_rate_limit.py file with this reference.

import requests

# Target one public project response to keep the payload small
URL = "https://gitlab.com/api/v4/projects?per_page=1"

# List every GitLab rate-limit header the script can report
RATE_LIMIT_HEADERS = (
    "RateLimit-Limit",
    "RateLimit-Name",
    "RateLimit-Observed",
    "RateLimit-Remaining",
    "RateLimit-Reset",
    "RateLimit-ResetTime",
    "Retry-After",
)

# Send the request without authentication headers
response = requests.get(URL)

# Show the result before printing available quota details
print(f"HTTP status: {response.status_code}")
print("Rate limit headers:")
for header in RATE_LIMIT_HEADERS:
    value = response.headers.get(header)
    if value is not None:
        print(f"{header}: {value}")
Run the anonymous request

The response gives you a snapshot of the quota attached to your current IP address. No token is present to identify your GitLab account.

Before you run the script, do you expect the remaining quota to equal the full limit after this request uses one call?

macOS

  • Run the script from your Desktop by running these commands:
cd ~/Desktop
python3 gitlab_rate_limit.py

The first line selects your Desktop. The second line executes gitlab_rate_limit.py with Python 3.

Windows

  • Run the script from your Desktop by running these commands in PowerShell:
Set-Location "$HOME\Desktop"
py gitlab_rate_limit.py

The first line selects your Desktop. The second line executes gitlab_rate_limit.py with the Python launcher.

You should see HTTP status: 200 followed by the available rate-limit headers. The quota values depend on recent anonymous requests from your IP address.

Don't see the status or quota headers?

If Python reports that Requests is unavailable, confirm that your terminal uses the same Python installation where the library was installed.

If the request cannot reach GitLab.com, check your internet connection before running the script again.

Help me troubleshoot my anonymous GitLab API request.

Your first anonymous call is working. You can now inspect the quota that GitLab attached to the response.

How do you read the quota?

  • The RateLimit-Limit header shows the request quota for the current window.
  • The RateLimit-Observed header shows the requests counted in that window.
  • The RateLimit-Remaining header shows how many requests remain.
  • The RateLimit-Reset header gives the Unix timestamp for the next quota reset.

You have your anonymous quota baseline. Next, you'll watch that remaining count fall until GitLab refuses another request.

Trigger the Anonymous Limit

Your first unauthenticated call showed how the GitLab.com REST API exposes its anonymous quota through response headers. The remaining count becomes useful when you watch it fall toward a rate limit.

Now you will keep sending the same request until the API responds with HTTP 429. The limiting response contains the reset information your script needs to report before it stops.

In this step, get ready to:
  • Repeat the existing unauthenticated request.
  • Print the status plus available rate-limit headers for every response.
  • Capture HTTP 429 with reset information before stopping.
Build the stopping loop

The response output needs to run before the stop check. This order keeps the limiting response visible in your terminal.

How should the loop flow?

  • The existing anonymous request runs once per loop pass.
  • The existing status plus available rate-limit header output runs for every response.
  • The script checks for HTTP 429 after printing each response.
  • The limiting response reports its reset header or reset-time information before the loop ends.
  • Switch back to gitlab_rate_limit.py in the editor from earlier.
  • Update gitlab_rate_limit.py so its control flow matches the sequence above.
  • Save gitlab_rate_limit.py.

✔️ Awesome, I've got everything!

Your saved script now repeats the anonymous request. It reports the limiting response before it stops.

ⓧ I'd like to double check the full code

  • Confirm that gitlab_rate_limit.py repeatedly sends unauthenticated requests to the same public GitLab endpoint.
  • Confirm that every response prints its status plus all available rate-limit headers.
  • Confirm that the HTTP 429 path prints reset header or reset-time information before stopping.
Run into the anonymous limit

Every successful anonymous call spends part of the current allowance. The repeated output lets you watch that allowance disappear.

Using the full allowance can feel risky. Your stop condition ends the loop as soon as HTTP 429 has been reported.

Before you run it, do you think the first response will show HTTP 429 or more anonymous quota to spend?

  • Return to the terminal from the previous step.
  • Start gitlab_rate_limit.py with the same Python command you used earlier.

At first, you should see a status plus available rate-limit headers for each call. The remaining quota should decrease across successive response groups.

The final response group should show HTTP 429. Your script should print reset header or reset-time information before it stops.

You caught the anonymous ceiling in action. Your script now preserves the exact information exposed when the quota runs out.

Loop Stops Too Early or Too Late?

  • If the script prints one response, make sure the repeated flow contains the request plus its output.
  • If requests continue after HTTP 429, make sure the loop stops immediately after reporting the reset information.
  • If reset information is missing, make sure the limiting response uses the same available-header output as the earlier responses.

Help me debug the anonymous request loop.

The anonymous ceiling is now visible. Next, you will authenticate the same request with a personal access token to compare its quota.

Authenticate With a GitLab Token

Your anonymous run proved how quickly a public script can exhaust its quota. The next goal is to identify your requests so GitLab can apply the authenticated allowance.

A personal access token authenticates your REST API calls. An environment variable keeps that credential outside gitlab_rate_limit.py.

In this step, get ready to:
  • Create a GitLab personal access token with the read_api scope.
  • Store the token in the GITLAB_TOKEN environment variable.
  • Run an authenticated request to compare its remaining quota with your anonymous result.
Create a narrow GitLab token

A token scope controls what the credential can do. The read_api scope provides the read access this request needs without granting write access.

This part creates a live credential. GitLab displays the token once, so keep the value private while you move it into your terminal.

  • Go to GitLab.com in your browser.
  • Sign in to your GitLab account.
  • Select your avatar in the upper-right corner.
  • Select Edit profile.
  • Select Access in the left sidebar.
  • Select Personal access tokens.

You should now see the page where GitLab creates and manages personal access tokens.

  • Select Generate token.
  • Select Legacy token from the dropdown list.
  • Enter gitlab-rate-limit-demo in the Token name field.
  • Choose tomorrow's date in the Expiration date field.
  • Select only read_api in the scopes list.
  • Select Generate token.
  • Copy the displayed token to your clipboard.

Why Use the Narrow Scope?

The read_api scope grants read access to the API. Your script only lists public projects, so write access would add unnecessary permission.

The sensitive part is complete. Your token now has the access needed for this read-only API call.

Store the token outside your script

An environment variable passes the token to Python without placing it in your source file. The value stays in the current terminal session.

macOS

The command pauses at a hidden prompt. Your pasted token does not appear on the screen.

  • Set GITLAB_TOKEN in the terminal from earlier by running these commands:
read -s "GITLAB_TOKEN?Paste your GitLab token: "
export GITLAB_TOKEN
echo
test -n "$GITLAB_TOKEN" && echo "GITLAB_TOKEN is set"

What Do These Commands Do?

  • The hidden prompt captures the token without echoing it to the terminal.
  • The export command makes GITLAB_TOKEN available to the Python process.
  • The final check confirms that a value exists without printing the credential.

Windows

PowerShell masks the token while you paste it. The final line confirms the environment variable without revealing its value.

  • Set GITLAB_TOKEN in the terminal from earlier by running these commands:
$secureToken = Read-Host "Paste your GitLab token" -AsSecureString
$credential = New-Object System.Management.Automation.PSCredential ("token", $secureToken)
$env:GITLAB_TOKEN = $credential.GetNetworkCredential().Password
if ($env:GITLAB_TOKEN) { "GITLAB_TOKEN is set" }

What Do These Commands Do?

  • PowerShell captures the pasted token as a masked secure string.
  • The credential object passes the value into GITLAB_TOKEN for the current PowerShell session.
  • The final check confirms that the environment variable contains a value.

Linux

The Bash prompt captures the token silently. The confirmation line reveals only whether the variable contains a value.

  • Set GITLAB_TOKEN in the terminal from earlier by running these commands:
read -s -p "Paste your GitLab token: " GITLAB_TOKEN
echo
export GITLAB_TOKEN
test -n "$GITLAB_TOKEN" && echo "GITLAB_TOKEN is set"

What Do These Commands Do?

  • The silent prompt reads the token without displaying it.
  • The export command makes GITLAB_TOKEN available to the Python process.
  • The final check confirms that the variable contains a value without exposing it.

Don't See the Confirmation?

Run the commands again in the same terminal that you use for the Python script. Make sure you paste the full token when the hidden prompt waits for input.

If you closed the token page before copying the value, GitLab cannot display it again. Create a replacement token with the same narrow scope.

Help me set the GitLab token safely

Compare authenticated and anonymous quota

Your script needs to look for GITLAB_TOKEN each time it starts. When the variable exists, the request sends GitLab's recommended PRIVATE-TOKEN header.

  • Switch back to gitlab_rate_limit.py in the editor from the earlier steps.
  • Press Cmd+A (macOS) or Ctrl+A (Windows and Linux) to select the existing code.
  • Replace the selected code by pasting the following script:
import os

import requests

# Keep the public endpoint in one place for both request modes.
URL = "https://gitlab.com/api/v4/projects?per_page=1"
# Track every rate-limit header GitLab may include.
RATE_LIMIT_HEADERS = (
    "RateLimit-Limit",
    "RateLimit-Remaining",
    "RateLimit-Reset",
    "RateLimit-ResetTime",
    "Retry-After",
)

# Add authentication only when the environment variable exists.
token = os.getenv("GITLAB_TOKEN")
headers = {"PRIVATE-TOKEN": token} if token else {}
mode = "authenticated" if token else "anonymous"

# Send one request so both modes are easy to compare.
response = requests.get(URL, headers=headers)
print(f"Mode: {mode}")
print(f"Status: {response.status_code}")

# Print every available rate-limit header from the response.
for header_name in RATE_LIMIT_HEADERS:
    value = response.headers.get(header_name)
    if value is not None:
        print(f"{header_name}: {value}")

What Does This Code Do?

  • The os.getenv() call reads GITLAB_TOKEN from the terminal environment.
  • The headers dictionary adds token authentication only when the variable contains a value.
  • The mode label makes the active request type visible in your output.
  • The loop prints each rate-limit header that GitLab includes in the response.
  • Save gitlab_rate_limit.py by pressing Cmd+S (macOS) or Ctrl+S (Windows and Linux).

✔️ Awesome, I've got everything!

Your script reads the token from the environment without storing the credential in gitlab_rate_limit.py.

ⓧ I'd like to double check the full code

Compare your saved gitlab_rate_limit.py with this complete version.

import os

import requests

# Keep the public endpoint in one place for both request modes.
URL = "https://gitlab.com/api/v4/projects?per_page=1"
# Track every rate-limit header GitLab may include.
RATE_LIMIT_HEADERS = (
    "RateLimit-Limit",
    "RateLimit-Remaining",
    "RateLimit-Reset",
    "RateLimit-ResetTime",
    "Retry-After",
)

# Add authentication only when the environment variable exists.
token = os.getenv("GITLAB_TOKEN")
headers = {"PRIVATE-TOKEN": token} if token else {}
mode = "authenticated" if token else "anonymous"

# Send one request so both modes are easy to compare.
response = requests.get(URL, headers=headers)
print(f"Mode: {mode}")
print(f"Status: {response.status_code}")

# Print every available rate-limit header from the response.
for header_name in RATE_LIMIT_HEADERS:
    value = response.headers.get(header_name)
    if value is not None:
        print(f"{header_name}: {value}")

Before you run the script, which remaining count do you expect to be larger: the anonymous result from earlier or this authenticated result?

  • Run the updated script in the same terminal where you set GITLAB_TOKEN by using this command:
python3 gitlab_rate_limit.py

What Should You See?

You should see Mode: authenticated followed by a successful status code. The script should also print the available rate-limit headers.

Compare RateLimit-Remaining with the anonymous value you observed earlier. The authenticated request should show the higher allowance attached to your account.

Still Seeing Anonymous Mode?

The Python process can only read variables from the terminal that launched it. Set GITLAB_TOKEN again if you opened a new terminal session.

If authentication fails, create a replacement token with the read_api scope. Paste it again through the hidden terminal prompt.

Help me debug my authenticated request

That comparison proves your script can switch request identities without exposing the token in source code. The authenticated quota is now visible beside the anonymous result from your earlier run.

Your script now authenticates safely through its environment. Next up, you'll use the reset information to wait before retrying a rate-limited request.

Retry After the Rate Limit Resets

Your token comparison against the GitLab API proved that authentication changes the quota available to your script. Every client can still reach an HTTP 429 when it sends enough requests.

A resilient client uses backoff to turn the reset headers into its next action. You will make the script wait for the reset time before it retries once.

In this step, get ready to:
  • Add a reset-aware wait routine to the script.
  • Retry the request once after the quota resets.
  • Confirm recovery after reaching the anonymous limit.
Add the reset-aware retry

A throttled GitLab response includes Retry-After as a countdown in seconds. The response also includes RateLimit-Reset as the Unix time when the quota resets.

The script uses the countdown first. It falls back to the reset timestamp when the countdown is unavailable.

  • Replace the contents of gitlab_rate_limit.py with this completed version:
import os, sys, time, requests

URL = "https://gitlab.com/api/v4/projects?per_page=1"
RATE_LIMIT_HEADERS = ("RateLimit-Limit", "RateLimit-Remaining", "RateLimit-Reset", "RateLimit-ResetTime", "Retry-After")

# Send one request and print every available rate-limit header.
def call_gitlab():
    token = os.getenv("GITLAB_TOKEN") if "--anonymous" not in sys.argv else None
    response = requests.get(URL, headers={"PRIVATE-TOKEN": token} if token else {}, timeout=30)
    print(f"Status: {response.status_code}")
    for name in RATE_LIMIT_HEADERS:
        if name in response.headers:
            print(f"{name}: {response.headers[name]}")
    return response

# Wait until GitLab says the current quota window has reset.
def wait_for_reset(response):
    retry_after = response.headers.get("Retry-After")
    wait_seconds = int(retry_after) if retry_after else max(int(response.headers["RateLimit-Reset"]) - int(time.time()), 0)
    print(f"Waiting {wait_seconds} seconds for the quota to reset.")
    time.sleep(wait_seconds + 1)

# Drive requests to 429 before waiting and retrying exactly once.
while True:
    response = call_gitlab()
    if response.status_code == 429:
        wait_for_reset(response)
        call_gitlab()
        break
    time.sleep(1)

What does this code do?

  • The call_gitlab() function sends one request through Requests. It prints the status code plus every available rate-limit header.
  • The --anonymous option skips the token for one run. Your terminal keeps its existing GITLAB_TOKEN value.
  • The wait_for_reset() function converts GitLab's reset information into a sleep duration.
  • The main loop stops after one post-reset retry. This prevents an unlimited retry cycle.
  • Save gitlab_rate_limit.py.

✔️ Awesome, I've got everything!

Your saved script now reads the reset information before retrying once.

ⓧ I'd like to double check the full code

Compare your saved gitlab_rate_limit.py with this complete version.

import os, sys, time, requests

URL = "https://gitlab.com/api/v4/projects?per_page=1"
RATE_LIMIT_HEADERS = ("RateLimit-Limit", "RateLimit-Remaining", "RateLimit-Reset", "RateLimit-ResetTime", "Retry-After")

# Send one request and print every available rate-limit header.
def call_gitlab():
    token = os.getenv("GITLAB_TOKEN") if "--anonymous" not in sys.argv else None
    response = requests.get(URL, headers={"PRIVATE-TOKEN": token} if token else {}, timeout=30)
    print(f"Status: {response.status_code}")
    for name in RATE_LIMIT_HEADERS:
        if name in response.headers:
            print(f"{name}: {response.headers[name]}")
    return response

# Wait until GitLab says the current quota window has reset.
def wait_for_reset(response):
    retry_after = response.headers.get("Retry-After")
    wait_seconds = int(retry_after) if retry_after else max(int(response.headers["RateLimit-Reset"]) - int(time.time()), 0)
    print(f"Waiting {wait_seconds} seconds for the quota to reset.")
    time.sleep(wait_seconds + 1)

# Drive requests to 429 before waiting and retrying exactly once.
while True:
    response = call_gitlab()
    if response.status_code == 429:
        wait_for_reset(response)
        call_gitlab()
        break
    time.sleep(1)
Force the limit and watch recovery

Anonymous mode lets you test the lower quota without removing the token from your terminal. The script sends one request per second until GitLab returns 429.

The pause can last for the rest of the current quota window. The printed countdown confirms that the script is waiting on purpose.

  • Test whether the retry clears the limit by running this command. Before you press Enter, predict whether the final status will match the first 429.
python3 gitlab_rate_limit.py --anonymous

You will first see requests with Status: 200 plus the available quota headers. Eventually, you will see Status: 429 followed by the printed wait duration.

After the pause, the next request should show Status: 200 with refreshed quota values. That is the recovery working: your script survives the limit by following GitLab's reset data.

Not reaching or recovering from 429?

Confirm that the command includes --anonymous. Without it, the script uses your token's higher quota.

If the anonymous quota recently reset, allow the requests to continue until the status changes. Keep the terminal running during the printed wait.

Check that wait_for_reset() uses the same response that returned 429.

Help me diagnose my rate-limit retry

Your script now treats a rate limit as timing data instead of a crash. Next, you will record the remaining quota from every call so you can see the limit fall and reset across the run.

Log Remaining Quota

Your script already survives a GitLab REST API rate limit. It reads the reset information from an HTTP 429 response before retrying.

Terminal output disappears into scrollback after a run. A plain-text quota log preserves every remaining count for later inspection.

In this step, get ready to:
  • Write each remaining-quota value to rate_limit_log.txt.
  • Run the script through another rate-limit recovery.
  • Open the log to see the quota decrease before refreshing.
Add the quota logger

The RateLimit-Remaining response header reports the available quota for the current window. Writing that value in append mode creates one continuous history.

  • Return to gitlab_rate_limit.py in the code editor from the earlier steps.
  • Place the following helper directly below the existing imports:
def log_remaining_quota(response):
    # Save one quota reading for each API response.
    remaining = response.headers.get("RateLimit-Remaining", "unavailable")
    with open("rate_limit_log.txt", "a", encoding="utf-8") as log_file:
        log_file.write(f"{remaining}\n")

What does this code do?

  • The response.headers.get() call reads the remaining quota without failing when the header is absent.
  • The fallback value unavailable keeps each response represented in the log.
  • Append mode places each reading on a new line after the previous entry.
  • Save gitlab_rate_limit.py.
  • Confirm your editor shows log_remaining_quota() without a Python syntax warning.

Seeing a syntax warning?

Check that the four indented lines sit inside log_remaining_quota(). Confirm that the final write statement contains matching parentheses and quotes.

Compare the indentation with the snippet above. Python requires each line in the function body to use the same indentation depth.

Help me fix the quota logger

Connect logging to each response

Your existing response-processing path prints the status code and rate-limit headers for every request. The same path also handles the response created by the retry.

  • Locate the response-processing block that prints each status code and its rate-limit headers.
  • Place the following call immediately after the existing header output:
# Record every response, including the retry after a rate limit.
log_remaining_quota(response)

Why log from this location?

Every response already passes through this part of the script. One logging call captures normal requests plus the response that follows the reset wait.

  • Save gitlab_rate_limit.py.

✔️ Awesome, I've got everything!

Your saved script now records the remaining quota whenever it prints a response summary.

ⓧ I'd like to double check the full code

Compare the two logging additions in gitlab_rate_limit.py with these reference snippets.

  • The helper appears once below the existing imports.
def log_remaining_quota(response):
    # Save one quota reading for each API response.
    remaining = response.headers.get("RateLimit-Remaining", "unavailable")
    with open("rate_limit_log.txt", "a", encoding="utf-8") as log_file:
        log_file.write(f"{remaining}\n")
  • The logger call appears once in the shared response-processing path.
# Record every response, including the retry after a rate limit.
log_remaining_quota(response)

Before you run the script again, consider this question: will the retry response create its own line in the log.

  • Switch back to the terminal from the previous step.
  • Repeat the same script command you used to force the rate limit earlier.
  • Leave the process running until its reset wait finishes.

While the script waits

A quiet terminal can feel stuck during this part. The reset header controls the pause, so leave the process running until the retry response appears.

After the first response, you should see rate_limit_log.txt appear beside gitlab_rate_limit.py in your editor.

Log file missing?

Confirm that log_remaining_quota(response) sits in the path that prints every response. Check that you saved gitlab_rate_limit.py before repeating the run.

Help me find the missing log file

Inspect the quota history

The terminal showed each response one at a time. The log now places those readings together so the full quota pattern is visible.

Before you open the file, consider where the refreshed value should appear relative to the lowest value.

  • Select rate_limit_log.txt in your editor's file browser.

You should see one remaining-quota value for every response. The values decrease across the run.

After the rate-limit recovery, you should see a refreshed value below the lowest value. That final jump proves the retry received a new quota reading.

  • Compare the number of lines in rate_limit_log.txt with the response summaries from the terminal.

Missing the refreshed value?

Confirm that the terminal reached 429 before waiting. Keep the script running until the retry response prints.

If normal responses are logged but the retry is missing, move the logging call into the shared response-processing path used by both request attempts.

Help me troubleshoot the quota history

You have turned a stream of temporary response headers into a persistent quota history. Your script now records the decline and the recovery in one file.

Secret mission

Audit Your Rate-Limit Recovery

Use the quota log you already produced to reconstruct the moment your script recovered from the rate limit. Turn the raw entries into evidence that explains how much quota returned after the reset.

Clean Up Your Resources

Clean Up Your Resources

Keeping these resources costs nothing. Choose whether to keep them ready, pause your work, or delete everything created for this run.

Resources you used:

  • A GitLab personal access token with the read_api scope.
  • The GITLAB_TOKEN value in your current terminal environment.
  • The local gitlab_rate_limit.py script.
  • The local rate_limit_log.txt quota log.

Keep everything running

No action needed. Choose this if you plan to test the rate-limit recovery again soon.

  • Keep the GitLab personal access token active.
  • Keep the current terminal session open if it is the only place holding your token value.
  • Keep gitlab_rate_limit.py in its current location.
  • Keep rate_limit_log.txt as a record of the quota changes you observed.

Pause - I'll come back to this later

There is no background process between script runs. Pausing means waiting before your next API call while preserving your work.

  • Leave the current terminal session open so GITLAB_TOKEN remains available.
  • Keep the GitLab personal access token active.
  • Leave gitlab_rate_limit.py idle until you are ready to continue.
  • Resume testing with the same run command you used earlier.

Delete - I don't want to use this again

Remove the token plus the local files if you have finished the experiment. This gives you a clean starting point for a future run.

Revoking the token is permanent, which is the safe outcome for a one-off run. You can create a new token later if you repeat the project.

  • Return to the GitLab personal access token page you used earlier.
  • Locate the token with the read_api scope.
  • Use the token's revocation control.
  • Confirm the token no longer appears as active.
  • Close the terminal session that contains GITLAB_TOKEN.

The terminal-specific GITLAB_TOKEN value is now cleared from that session.

Finding the local files can be fiddly if you saved them outside your usual projects folder.

  • Choose your operating system below to delete the local project files.

macOS

  • Open Finder from the Dock.
  • Search your Mac for gitlab_rate_limit.py.
  • Delete gitlab_rate_limit.py from the search results.
  • Search your Mac for rate_limit_log.txt.
  • Delete rate_limit_log.txt from the search results.
  • Review your deleted items to confirm they only contain files you are willing to remove permanently.
  • Permanently remove the two project files.
  • Search again for gitlab_rate_limit.py to confirm it is gone.
  • Search again for rate_limit_log.txt to confirm it is gone.

Windows

  • Open File Explorer from the taskbar.
  • Search your PC for gitlab_rate_limit.py.
  • Delete gitlab_rate_limit.py from the search results.
  • Search your PC for rate_limit_log.txt.
  • Delete rate_limit_log.txt from the search results.
  • Review your deleted items to confirm they only contain files you are willing to remove permanently.
  • Permanently remove the two project files.
  • Search again for gitlab_rate_limit.py to confirm it is gone.
  • Search again for rate_limit_log.txt to confirm it is gone.

Nice Work!

Nice Work!

You did it! You built a Python script that tracks the rate limit on every GitLab.com REST API response. It now recovers from HTTP 429 using the reset information returned by GitLab.com.

You've learned how to:

  • Compare anonymous and authenticated requests using the status code plus the rate-limit headers returned with each response.
  • Handle HTTP 429 recovery using the response's reset-time information. Your script waits for the quota to refresh before retrying once.
  • Record the remaining quota from every response in rate_limit_log.txt. The log shows the quota decrease across calls before refreshing after recovery.
  • Secret Mission: Complete an optional project extension that pushes your API resilience skills further.

Ready to quiz yourself?