Everything on this page is measured live against the production index. Nothing is a screenshot, a claim, or a number typed in by hand.
Four questions, in the order most people ask them:
- How much is there? The tiles.
- Do you have my slice? Search a job title, or break the corpus down by any dimension.
- Do you track my companies? Switch the search to Companies and type a name.
- Is it any good? Freshness and quality signals are further down this page.
One job means one open hire
Every count here is a distinct role a company is actively hiring for. A single role posted across several locations — Software Engineer, NYC / SF / Remote — is one job in this corpus. Most providers count it as three.
So when you compare our totals against someone else's, expect a smaller number. It is not less coverage; it is the same hiring measured without the duplication.
Using the explorer
| To do this | Do that |
|---|---|
| Narrow to a role | Type a job title in the search box |
| Check a company is tracked | Switch the toggle to Companies, type the name |
| Focus on one company | Pick it from the results — the whole page narrows to it |
| Narrow to a place, level, skill… | Click any bar in the breakdown |
| Combine narrowings | Keep going — filters compose, and each shows as a removable chip |
| Share what you are looking at | Copy the URL; the filters and the company lookup travel with it |
Two things worth knowing. Picking a second value on the same dimension replaces the first rather than adding to it — a posting cannot be in two countries at once. And the tiles always describe the whole corpus; the filtered total appears under the chips, because the hourly rollup behind the tiles counts the index rather than your query.
Why the tiles and the filtered total come from different places
Headline totals prefer an authoritative rollup over a raw index count. The indexes are
continuously rebuilt, so an unfiltered index total under-reports while a rebuild is in
flight. Every response is therefore stamped with the hourly jobfront_metrics rollup
(rds_total_count, rds_total_past_7d, rds_sources_total_count) and the tiles prefer
it, which is why the number here matches the number in the dashboard.
Once you filter, the index wins
The rollup only answers the unfiltered question. As soon as a filter is applied — a country, a segment, a date window — only the index can count that slice, and its number is the correct one. That is the number that appears under the chips.
Break it down by
The chips come from the live segment registry rather than a hardcoded list, so what you see on the page is the authoritative set.
| Segment | What it answers |
|---|---|
| Occupation (O*NET) | Volume by standard occupation code, so you can map our corpus onto a taxonomy you already use |
| Metro (CBSA) | Volume by US metro area, grouped by state |
| Country | Volume by country across the whole corpus |
| State / Region | Volume by state or region |
| City | Volume by city |
| Level | Seniority mix, from internship to executive |
| Commitment | Full-time, contract and the rest of the employment set |
| Workplace Type | On-site, hybrid and remote work modes |
| Skill | Volume by skill extracted from the posting |
| Top Companies | The largest employers in the corpus, ranked by job volume |
| Remote, Internships | Filter slices: pick one and the corpus is narrowed to that flag, then broken down by country |
Metro, State and City come back grouped by their parent — cities under their state, states under their country — because there are a dozen Springfields.
Location buckets count location entries
Metro, city, state and country buckets aggregate over the nested job_locations array,
so a posting that lists several locations contributes to each one. Read them as
"postings available in this place", not as a partition of the corpus — the buckets can
add up to more than the active-job total.
Bars scale to the largest bucket in view, not to the total: job data is long-tailed, and scaling to the total collapses every bar after the first into a sliver. Read the bars for shape and the numbers for magnitude. The share column beside each count is additive. "Still in the tail" is what fell outside the returned top-N — a large tail is normal for occupation, skill and city, and walking one properly is what the V4 API is for.
Three segments deserve a note on how their buckets are built.
Occupation is resolved, not raw
Buckets aggregate the job_onet_code on each posting,
then resolve the code to a human occupation title from an embedded O*NET-SOC map,
falling back from 8-digit to 6-digit to the 2-digit major group. Labels read as
title · code. No external O*NET call is made.
Noisy values are canonicalised
Level, commitment and workplace type run through an alias map before counting, so
sr, Sr. and SR all land in one
senior bucket instead of fragmenting the distribution across spellings.
Top Companies is job-weighted
This is the one segment sourced from the companies index rather than the jobs index, and it sums each company's job-board job count instead of counting documents — so it ranks by hiring volume, not by how many company records exist.
How fresh is it?
Postings are discovered by crawling company job boards directly, then parsed, enriched and made servable on the same pass. There is no overnight batch between ingest and availability, which is why the median posting age is a number about the market rather than about our pipeline.
What each number means
Active jobs
Postings that are servable right now: job_status: active in the jobs index. This is the
denominator for everything else on the page, and the population every breakdown above
divides up. It is the same active-posting predicate the public API applies, so a total you
read here lines up with a total you get back from /v4/jobs — the same corpus, filtered
the same way.
Companies
Sources that are both active and have a tracked job board (is_active and
has_jobboard). Company profiles we track but do not scrape are deliberately excluded —
counting them would inflate the number without adding a single retrievable posting.
Ingested last 24h
Active postings whose created_at — the timestamp we first saw the posting — falls in
the last 24 hours. It is deliberately not updated_at: a re-scrape that touches every
record would otherwise read as a giant ingest spike. The sparkline is the same window as a
per-hour histogram, zero-filled across all 24 buckets so a quiet stretch renders as a flat
line rather than a gap.
Median posting age
The p50 of created_at across the active corpus, expressed in days. A median, not a mean:
job data has a long tail of evergreen listings that sit open for months, and a mean would
let that tail drag the number well past what a typical live posting looks like.
Job-board platforms
The count of distinct job-board formats parsed across those sources.
Quality signals
Coverage answers how much. These answer how good — three genuinely different things, reported separately rather than blended into a single score that would mean nothing.
Source quality bands describe how rich the underlying posting was. A low band is
usually a thin posting — a title and a link, with little to enrich from — rather than a
failed parse. Filter on data_quality_band if your use case needs the richer end of the
corpus.
Workplace classifier confidence is per-record rather than a global claim. The
enrichment models emit a confidence between 0 and 1 on every workplace tagging, and set
conflicting when the signals in a posting actively disagreed. A high conflict rate on a
slice is the pipeline telling you it was unsure, which is more useful than a confident
guess. Both are on the object, so you can
require agreement rather than trusting a coin flip.
What these numbers are
Three things the system computes about itself: how rich the source was, how confident the classifier was, and how often its signals disagreed. They describe the pipeline's view of its own work on each record — read them per-slice, not as a headline score.
The sample panels
Representative jobs is a random recent slice, not the head of the index — the sample is drawn with a random score, so it changes between refreshes and you see the ordinary case rather than a curated one. With no filters applied it is held to a quality floor (recent, with a location, compensation and a description) so the panel shows what a populated row actually looks like. Applying a filter drops those floors — layering them on a narrow query would frequently return nothing.
The response is trimmed before it leaves the service: raw posting HTML, the full extracted dictionary, internal ids and computed scores are removed, descriptions are capped at 600 characters, and posting URLs are stripped — retrieving those is what the API is for.
Top companies ranks by active job-board job count, restricted to active companies with a tracked job board and at least one active job. With a job-title search applied it changes meaning and ranks companies by how many matching jobs they have — "who is actually hiring for this role".
Not seeing a company you need?
The lookup covers companies we already track. Anything missing can be added as a custom source — that is a support request, not a limit of the dataset. If you want a whole target list checked at once, send it over and we will run it against the index.
The coverage API
PUBLIC ENDPOINTS
Everything the explorer renders is available as JSON. These endpoints are public,
read-only, aggregate and PII-free, and they are served by the docs host rather than by
api.jobfront.com. No token is required. Each is also reachable under the
/api/data-coverage/... prefix.
| Endpoint | Returns | Cache TTL |
|---|---|---|
GET /data-coverage/overview |
Global counts: active jobs, posted 1d/7d/30d, remote, internship, visa-sponsorship, with-compensation; plus company totals and computed_at |
5 min |
GET /data-coverage/segments |
The segment registry — each segment's id, label, backing index, short tag, and the force_filters it applies before aggregating |
in-process metadata, not cached |
GET /data-coverage/segment?segment=&top_n=&filters= |
Top-N buckets for one segment, the total in scope, hierarchy_group_by where the buckets nest, each bucket's pct and filter_type, and has_more with remaining_count for the tail |
60 s |
GET /data-coverage/jobs?limit=&filters= |
A random recent representative sample, field-trimmed as described above | 30 s |
GET /data-coverage/sources?limit=&filters= |
Top companies by job volume, or matching companies by name when search_scope=sources |
60 s |
Filters compose across segments
filters takes a JSON object on the query string; the same keys are also accepted as
individual query parameters. Filters apply to every endpoint that takes them, and they
compose — narrow by country and remote flag, then break the result down by occupation.
curl -sG https://docs.jobfront.com/data-coverage/segment \
--data-urlencode 'segment=job_onet_code' \
--data-urlencode 'top_n=10' \
--data-urlencode 'filters={"location_country":"us","is_remote":true}'
Checking whether a company is tracked is the same call against sources:
curl -sG https://docs.jobfront.com/data-coverage/sources \
--data-urlencode 'filters={"search":"stripe","search_scope":"sources"}'
Recognised keys are search, search_scope, search_in_description, posted_window,
job_category, job_department, job_level, job_commitment, job_workplace_type,
job_industry, source_industry, job_skill, technology_slug, team_family,
job_onet_code, ats_family, data_quality_band, location_country, location_state,
location_city, location_cbsa, source_brand_name, is_remote, is_internship and
is_offers_visa_sponsorship.
Anything else is dropped before the request reaches the cluster. Free text is trimmed to 120 characters, and a description-scoped search shorter than 3 characters silently loses the description scope — at that length it is a near-full-text scan, and the title-only path still answers.
Industry is a filter, not a segment
job_industry and source_industry narrow the corpus, but industry is not one of the
break-down dimensions in the registry. To size an industry, filter by it and then break
the result down by occupation, metro or level.
The live aggregate endpoints
A second family answers the unfiltered, whole-corpus questions — the numbers behind the
tiles and the freshness row above. They take no filters. Same guarantees as the rest:
public, read-only, aggregate, PII-free, served by the docs host, no token.
curl -s https://docs.jobfront.com/docs/live/summary
| Endpoint | Returns | Cache TTL |
|---|---|---|
GET /docs/live/summary |
Headline counts: active jobs, companies, postings first seen in 24h, remote count, jobs with compensation, median posting age, distinct job-board platforms | 60 s |
GET /docs/live/ingest-24h |
points[] of ts and count, one per hour, zero-filled across the window, plus a total |
60 s |
GET /docs/live/coverage |
by_source_type[] with each type's share of the corpus, by_country[] (top 12), the distinct platform count and the company total |
60 s |
GET /docs/live/metros?limit= |
metros[] with CBSA title, 5-digit code, active-job count and share. limit defaults to 8 and is clamped to 1–50 |
60 s |
One further endpoint runs a whole-corpus aggregation. It is much wider than the ones above and is cached for one hour rather than 60 seconds:
| Endpoint | Returns | Cache |
|---|---|---|
GET /docs/live/quality |
Source quality bands, workplace classifier confidence, and the rate at which its signals conflicted. Powers the quality panel above | 1 h |
Source type is available here, not in the explorer
by_source_type on /docs/live/coverage is the corpus split by source type, normally
weighted by jobs — the sum of each type's job-board job counts, not a count of
companies. Weighting by company count would mostly tell you that most companies run
their own ATS. Source type is not one of the break-down dimensions in the segment
registry, so this endpoint is the way to ask that question.
Check source_type_weighting before you read the shares. When the job counts are
unpopulated the endpoint falls back to counting companies rather than returning an
empty split, and reports source_type_weighting: "companies" instead of "jobs". In
that case the shares are company share, not job share.
Response keys on /docs/live/summary
| Key | What it is |
|---|---|
updated_at |
Epoch seconds when the aggregation ran. Drives the "updated Ns ago" chip. |
active_jobs |
Active postings in the jobs index. |
posted_24h |
Active postings first seen in the last 24 hours. |
remote_jobs |
Active postings flagged remote. |
jobs_with_compensation |
Active postings carrying a maximum compensation value. |
median_posting_age_days |
p50 of created_at in days. Omitted when the percentile does not resolve. |
companies |
Active sources with a tracked job board. |
ats_platforms |
Distinct job-board platforms across those sources. |
rds_total_count, rds_total_past_7d, rds_sources_total_count |
Authoritative rollup counts, stamped on every response from this service. |
latency_ms |
Server-side time to produce the response. |
jobs_error, sources_error |
Present only when one cluster failed. The rest of the payload is still valid. |
Cross-origin GET is allowed on purpose: these are aggregate, non-sensitive numbers and
you are welcome to build your own dashboard on them.
Guardrails
Each call fans out to a live cluster aggregation, so the endpoints are guarded. All of these limits are enforced per worker, so treat them as protection against abuse rather than as a published quota:
| Guard | Limit | Response |
|---|---|---|
| Per-client burst | 20 requests per 10 seconds, keyed by Bearer token when present, otherwise by IP | 429 with Retry-After |
| Global cap | 600 requests per 60 seconds per worker | 503 with Retry-After |
| Concurrency | At most 8 in-flight cluster-bound requests, with a short queue wait | 503 |
| Query-string size | 4 KB | 413 |
User-controlled knobs are clamped rather than trusted. top_n defaults to 25 with a hard
ceiling of 500, and the expensive high-cardinality segments — skill, occupation, city,
metro, state, top companies and the flag slices — are capped lower still. limit on the
jobs and sources endpoints defaults to 12 and is clamped to a maximum of 20.
If you need more than these endpoints comfortably give you, that is what the API is for — start at Get started and the V4 API reference. The v4 API has its own, separate limits — see Rate limits and billing.
When a panel is empty
Each panel on this page owns its own fetch and its own failure. A slow or failing aggregation greys out one card and offers a retry — it never blocks the page, and the documentation around it stays correct. Requests that take longer than 8 seconds are treated as a failure by the widget.
You will never see a stale number without a timestamp, and never a zero standing in for a missing value: an unavailable number renders as an em dash. If a panel is greyed while the rest of the page is fine, that is one aggregation timing out, not an outage.
Related
- Data dictionary — all ~57 fields with their types, absence contracts and provenance.
- How we get the data — the pipeline that produces everything counted here.
- Data quality — how the pipeline monitors itself.